测试案例的知识点有三个。
第一, 介绍测试案例相关的概念。
第二, 介绍测试案例的各种类别,
第三, 介绍测试案例的相关扩展,例如TestExecutionExceptionHandler,TestWatcher等。
1、概念
引用原著中对Test Plan, Test Class, Test Method, Lifecycle Method的概念。
Test Plan:Test plan is a hierarchical (and read-only) description of all engines, classes, and test methods that fit the LauncherDiscoveryRequest. The client can traverse the tree, retrieve details about a node, and get a link to the original source (like class, method, or file position). Every node in the test plan has a unique ID that can be used to invoke a particular test or group of tests.
Test Plan它代表一次运行计划,它是树形结构,每个节点都抽象为Node。每个Node都与实际的资源(测试类,测试方法,测试文件路径)关联,每个Node都有一个唯一的ID。 树形结构是只读的,可以通过遍历树形结构获取Node的细节信息。
Test Class:any top-level class, static member class, or @Nested class that contains at least one test method
至少包含一个测试方法的外部类,内部类,使用@Nested标注的内部类。
Test Method:any instance method that is directly annotated or meta-annotated with @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, or @TestTemplate.
有上述注解中任意一种的实例方法,每种注解都对应一种测试案例类别.
Lifecycle Method: any method that is directly annotated or meta-annotated with @BeforeAll, @AfterAll, @BeforeEach, @AfterEach
有上述注解中任意一种的方法。
以生活为例,TestPlan,Node是一种逻辑结构,它类似于一个公司的组织结构图,每个图上的一个节点对应一个具体的子公司或者部门。Test class, Test method,Lifecycle Method是物理结构,一个节点一一对应一个物理结构。
2、类别
测试案例的类别有@Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate五种。
2.1 Test
它表示普通的测试案例,较常见,易理解。略。
2.2 RepeatedTest
它表示重复测试案例,value属性值表示重复的次数。它有特殊的属性。
currentRepetition, 当前为第几次执行。
totalRepetition,执行的总次数。
在方法上,可以添加它的独有参数RepetitionInfo,获取上述信息。
示例如下:
@RepeatedTest(3) void repeatTest(RepetitionInfo repeat){ System.out.println("repeated run test"); }
它的使用场景通常用于多次调用相同方法时,需要跳转到不同的逻辑分支,例如
- 使用手机注册,若第一次,则创建用户,并获取授权等等,若第二次,则直接进入登陆。
- 当客户付费时,相同渠道达到特定次数时,给与优惠,例如免除手续费等等。
2.3 ParameterizedTest
它表示提供一组模拟的数据,作为方法的参数。它必须提供模拟数据(方法参数)的方式。
当为单个参数时,参数来源有:
- 字面量,使用@ValueSource,它的值为字面量数组,例如参数为字符串,则为字符串数组。
- 特殊值,@NullSource, @EmptySource, @NullAndEmptySource。
- 枚举值,只适用于参数为枚举类型,@EnumSource。
- 方法返回值,当测试类不为PER_CLASS,必须是静态方法,使用@MethodSource,value属性指定方法名称。方法的返回值通常为Stream<T>。T对应测试案例的参数类型。
当为多个参数时,参数来源有:
- 表格(@CsvSource),每一行,一个字符串,每个字符串使用逗号分隔,可以设置delimiter属性自定义分隔符,分隔之后的数组个数,类型与参数对应。
- @CsvSourceFile,指定表格文件,内容个数与@CsvSource相同。
- @ArgumentSource,ArgumentsProvider的实现类,它的返回值为Stream<? extends Arguments>。
- @MethodSource, 它的返回值为Stream<Arguments>。每一组arguments与测试案例的方法对应。
示例如下:
@ParameterizedTest @MethodSource("testArgument") public void test(String key, String value) { System.out.println(key + " , " + value); } private static Stream<Arguments> testArgument() { return Stream.of(arguments("a", "b"), arguments("c", "d")); }
若提供的类型与方法参数类型不同时,还需要提供类型转换器,它类似于spring中的Converter,详细参考官网
它的使用场景很广泛,例如测试接口,将参数定义为JSONObject,提供不同的报文。
2.4 TestFactory
它表示动态的构建Test Plan,Test Plan是一棵树。它由一个或多个节点组成,每个节点对应具体的测试案例。
DynamicNode,它代表树上的一个节点。
DynamicContainer,它是容器,它代表一组节点,类似于文件夹与文件的关系。
DynamicTest,它代表一个具体的测试案例,若方法直接返回DynamicTest时,它与类对应的Node关联。
@TestFactory的方法返回值必须是DynamicNode对象。
示例如下:
第一种情况,返回DynamicTest,它对应top-level class的Node
@TestFactory Collection<DynamicTest> test(){ return Arrays.asList( DynamicTest.dynamicTest("test case 1", () -> System.out.println("one")), DynamicTest.dynamicTest("test case 2", () -> System.out.println("two"))); }
第二种情况,返回DynamicContainer,它有N组DynamicNode,每组的数据结构为Map,其中key值为组名,value为Stream<DynamicNode>,Collection< DynamicNode>或者是单个的DynamicNode。可以理解为文件的目录结构。
@TestFactory DynamicContainer test(){ return DynamicContainer.dynamicContainer("group 1",Stream.of("one","two")
.map(text -> DynamicTest.dynamicTest("test case 1", () -> System.out.println(text)))); }
2.5 TestTemplate
通用的测试案例模板,通常由第三方测试类库开发者实现自己的测试类别。
测试类型必须使用@ExtendsWith注解,值为TestTemplateInvocationContextProvider接口的实现类。
接口的方法有两个。
第一个,supportsTestTemplate,是否支持当前TestTemplate,通常为true,可以添加一些判断。
第二个,provideTestTemplateInvocationContext,含义是提供测试类的上下文,它的返回值是Stream<TestTemplateInvocationContext>对象。
TestTemplateInvocationContext有两个方法。
第一个,getDisplayName,获取测试案例的显示名称。
第二个,getAddtionalExtension(),等价于给测试案例注册扩展
示例参考官网。使用频率较低。
3、扩展
本篇介绍与测试案例有紧密关系的扩展。
3.1 TestWatcher
TestWatcher类似于监听器。它有四个方法。
testDisabled,当不满足条件,被跳过时,触发。
testAborted,当发生中断时,触发。
testSuccessful,当测试案例方法运行成功时,触发一次。
testFailed,运行失败时,触发。
示例,
public class TestWatcherSample implements TestWatcher { // log object private static Logger log = LoggerFactory.getLogger(TestWatcherSample.class); // @Override public void testDisabled(ExtensionContext context, Optional<String> reason) { log.info("------------disabled start------------"); log.info("displayName:{}", context.getDisplayName()); log.info("tagName:{}", context.getTags().toString()); log.info("className:{}, methodName:{}", context.getClass().getSimpleName(), context.getRequiredTestMethod().getName()); log.info("reason:{}", reason.get()); } @Override public void testSuccessful(ExtensionContext context) { log.info("------------successful start------------"); log.info("className:{}, methodName:{}", context.getClass().getSimpleName(), context.getRequiredTestMethod().getName()); } @Override public void testAborted(ExtensionContext context, Throwable cause) { log.info("------------aborted start------------"); log.info("className:{}, methodName:{}", context.getClass().getSimpleName(), context.getRequiredTestMethod().getName()); log.info("throwable:{}", cause.getMessage()); } @Override public void testFailed(ExtensionContext context, Throwable cause) { log.info("------------failed start------------"); log.info("className:{}, methodName:{}", context.getClass().getSimpleName(), context.getRequiredTestMethod().getName()); log.info("throwable:{}", cause.getMessage()); } } // Test case @ExtendWith(TestWatcherSample.class) public class WatchSample { // log object private static Logger log = LoggerFactory.getLogger(WatchSample.class); @Test public void test() { log.info("this is the sucessful case"); } @Disabled @Test public void disabled() { log.info("this is the disabled case"); } @Test public void failed() { log.info("this is the failed case"); log.info(1 / 0 + ""); } @Test public void testAborted() { log.info("this is the aborted case"); Assumptions.assumeTrue(false); log.info("aborted"); } }
3.2 TestExecutionHandler
Junit将异常分为两种,
第一种是在测试案例方法运行期间发生的异常,此时实现TestExecutionExceptionHandler。
第二种是在生命周期方法中发生的异常,此时实现LifeCycleExecutionExceptionHandler。接口方法名称格式为
handleXXXMethodExcutionException。XXX为生命周期的方法。每个生命周期在接口中都有对应的方法。例如beforeAll,对应的处理方法为handlerBeforeAllMethodExecutionException。
示例
public class ExceptionHandler implements TestExecutionExceptionHandler { // log object private static Logger log = LoggerFactory.getLogger(ExceptionHandler.class);
@Override public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable { if(throwable instanceof ArithmeticException){ log.info("just ignore this exception"); return; } throw throwable; } }
被异常处理器处理过之后未发生异常,则认为是成功的案例,触发testSuccessful,而不是testFailed。
3.3 ParameterResolver
为测试案例提供参数。它有两类:
第一类,是从上下文中获取信息,并构建参数对象。参考TestInfoParameterResolver。
第二类,是创建参数,并将其放入上下文中,在测试案例方法中获取。参考RandomParametersExtension
ParameterResolver,它主要有两个方法。
public interface ParameterResolver extends Extension {
boolean supportsParameter(ParameterContext var1, ExtensionContext var2) throws ParameterResolutionException; Object resolveParameter(ParameterContext var1, ExtensionContext var2) throws ParameterResolutionException; }
supportsParameter是判断当前的ParameterResolver支持的参数。若返回true表示支持,false表示不支持。
resolveParameter生成参数类型的一个实例对象。
两个方法中的参数都是一样的。其中parameterContext获取参数相关的上下文,ExtensionContext获取整个Test Plan 运行的上下文。
3.3.1 ParameterContext
获取位置:getIndex。
注解相关:
isAnnotated,判断参数上是否存在某个注解。
findAnnotation,获取参数上的注解。
findRepeatableAnnotation,获取参数上的注解,其类型必须是@Repeateable。
Executable:获取构造器或者方法。
综合信息:getParameter,返回Parameter对象
3.3.1 ExtensionContext
略。