JUnit 5 作为新一代的 Java 单元测试框架,提供很多改进。例如对比 JUnit4  与 JUnit5  的官网,JUnit5 的设计更加简约与时尚,至少不会抗拒阅读的程度了(像破烂一样的网站,看了整个人都难受,不影响效率?不存在的)
而且,除此外,他的文档使用了 Asciidoc, 相对于markdown复杂,主要是它还支持具有包含另一个文件内容,这对于写API文档来说挺重要的,有兴趣可以了解下~
Okay, 结束吐槽,让我来看看 JUnit5 到底带来了哪些变化吧
 
JUnit 5 是什么? 与以往的版本不同,JUnit5 由三个模块模版组成  JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform:运行测试框架的基础服务,定义了一套API,任何实现这套API的测试引擎,都能运行在这之上 
JUnit Jupiter:一系列用于编写JUnit5测试或者扩展的组合,同时他的子项目提供了JUnit5测试引擎 
JUnit Vintage:提供 JUnit3 和 JUnit4 的测试引擎 
 
三分钟教程 环境搭建 
创建你的项目(建议Spring Boot),简单的勾选几个依赖
 
添加 JUnit5 的依赖(spring boot 2.2 中已默认是Junit5,不需要额外加,详见WIKI ),
1 2 3 4 5 6 <dependency >    <groupId > org.junit.jupiter</groupId >    <artifactId > junit-jupiter</artifactId >    <version > ${latest-version}</version >    <scope > test</scope > </dependency > 
 
org.junit.jupiter:junit-jupiter已包含了 JUnit Platform,不需要额外声明依赖,一个就够了
 
 
第一个测试用例 
创建一个待测试的工具类
1 2 3 4 5 public  class  TimeUtils  {     public  static  String hello (Instant now)  {         return  "现在时间是:"  + now.toString();     } }
 
 
创建测试用例
1 2 3 4 5 6 7 8 class  TimeUtilsTest  {     @Test      void  hello ()  {         Instant  now  =  Instant.now();         String  expect  =  "现在时间是:"  + now.toString();         assertEquals(expect, TimeUtils.hello(now));     } }
 
 
运行测试用例,如果你使用idea,那么直接点旁边的运行按钮,或者使用其它编辑器的功能测试,当然,你还可以选择通过命令行,下载junit-platform-console-standalone ,并运行它(不懂),另一种是mvn test运行测试
 
 
更多食用方案 别名 测试的Class可以通过添加@DisplayName(),添加别名
1 2 @DisplayName("时间工具类测试") class  TimeUtilsTest  {}
 
也可以使用@DisplayNameGeneration(),进行更多的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @DisplayNameGeneration(TimeUtils2Test.ReplaceUnderscores.class) class  TimeUtils2Test  {     @Test      void  hello ()  {         Instant  now  =  Instant.now();         String  expect  =  "现在时间是:"  + now.toString();         assertEquals(expect, TimeUtils.hello(now));     }     static  class  ReplaceUnderscores  extends  DisplayNameGenerator .ReplaceUnderscores {         @Override          public  String generateDisplayNameForClass (Class<?> testClass)  {             return  "哈哈哈" ;         }     } }
 
断言、假设 测试中核心之一,用于判断是否执行成功,在JUnit5中增加了些对lambdas的支持,例如:
1 2 3 4 @Test void  asserts ()  {     assertEquals(1 ,2 , () -> "1要是1" ); }
 
另外,还增加了假设
1 2 3 4 5 6 7 8 9 10 11 12 @Test void  assume ()  {     assumingThat("DEV" .equals(System.getenv("ENV" )),             () -> {                                  assertEquals(1 , 1 );             });     assumeTrue("DEV" .equals(System.getenv("ENV" )),             () -> "Aborting test: not on developer workstation" );      }
 
禁用 添加@Disabled()可以禁用测试,这个意义在于某一测试用例遇到问题,临时不执行,等待问题修复后再次使用的
1 2 @Disabled("Disabled 因为重复") class  TimeUtilsCopyTest  {}
 
测试执行条件 通过添加 @EnabledOnOs 或者 @DisabledOnOs 来决定在某一操作系统上执行.
1 2 3 4 5 6 7 8 9 10 @Test @EnabledOnOs(MAC) void  testOnMac ()  {     log.info("exec on mac" ); }@Test @EnabledOnOs({ WINDOWS, LINUX }) void  testOnOs ()  {     log.info("exec on windows or linux" ); }
 
@EnabledOnJre 和 @DisabledOnJre 可以对java环境判断
1 2 3 4 5 @Test @EnabledOnJre(JRE.JAVA_8) void  testOnJava8 ()  {     log.info("exec on java 8" ); }
 
@EnabledIfSystemProperty/@DisabledIfSystemProperty 与  @EnabledIfEnvironmentVariable/@DisabledIfEnvironmentVariable 分别判断系统和环境变量,他们的匹配项支持正则表达式
1 2 3 4 5 @Test @DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*") void  notOnDeveloperWorkstation ()  {      }
 
标签/分组 JUnit5 中支持通过 @Tag() 对测试用例进行分组,例如
1 2 3 4 5 6 7 8 9 10 11 12 @Tag("conditional") @Test @EnabledOnOs(MAC) void  testOnMac ()  {     log.info("exec on mac" ); }@Tag("conditional") @Test @EnabledOnJre(JRE.JAVA_8) void  testOnJava8 ()  {     log.info("exec on java 8" ); }
 
@Tag() 有以下这些语法规则
不能为null或者空字符串 
不能有空格 
不能包含ISO控制符 
不能包含保留字符(,,(,),&,|,!) 
 
顺序 添加@TestMethodOrder(MethodOrderer.OrderAnnotation.class)与@Order(),定义测试用例的执行顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public  class  OrderedTest  {     @Test      @Order(2)      void  emptyValues ()  {              }     @Test      @Order(1)      void  nullValues ()  {              }     @Test      @Order(3)      void  validValues ()  {              } }
 
生命周期 JUnit5 提供了4个生命周期注解 @BeforeAll @AfterAll @BeforeEach @AfterEach
@BeforeAll:在所有的 @Test @RepeatedTest @ParameterizedTest @TestFactory 之前执行 
@BeforeEach:在每个测试用例前执行 
@AfterAll @AfterEach:与before类似,在测试用例之后执行 
 
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Slf4j @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public  class  LifecycleTest  {     int  num  =  0 ;     @BeforeAll      static  void  initAll ()  {         log.error("initAll" );     }     @BeforeEach      void  init ()  {         log.error("init" );     }     @Test      @Order(1)      void  doTest1 ()  {         log.error("num is "  + num);         num = 1 ;         log.error("doTest1" );     }     @Test      @Order(2)      void  doTest2 ()  {         log.error("num is "  + num);         num = 2 ;         log.error("doTest1" );     } }
 
除此外,还有@TestInstance()配置,见上面的例子,这个存在两个模式
PER_METHOD:每个测试用例执行前,都会创建一个实例(默认,与junit4一致) 
PER_CLASS:每个类的测试用例执行前,创建统一的实例 
 
上面的例子中,得到的log为:
1 2 3 4 5 6 7 13:58:03.477 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - initAll 13:58:03.485 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - init 13:58:03.487 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - num is 0 13:58:03.487 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - doTest1 13:58:03.494 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - init 13:58:03.495 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - num is 1 13:58:03.495 [main] ERROR com.jiangtj.example.junit5.LifecycleTest - doTest1
 
doTest1() 的执行,影响到num属性的值,而默认模式下则不会
嵌套 @Nested() 可以更好的表达测试用例间的关系,例如官方的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @DisplayName("A stack") class  TestingAStackDemo  {     Stack<Object> stack;     @Test      @DisplayName("is instantiated with new Stack()")      void  isInstantiatedWithNew ()  {         new  Stack <>();     }     @Nested      @DisplayName("when new")      class  WhenNew  {         @BeforeEach          void  createNewStack ()  {             stack = new  Stack <>();         }         @Test          @DisplayName("is empty")          void  isEmpty ()  {             assertTrue(stack.isEmpty());         }         @Test          @DisplayName("throws EmptyStackException when popped")          void  throwsExceptionWhenPopped ()  {             assertThrows(EmptyStackException.class, stack::pop);         }         @Test          @DisplayName("throws EmptyStackException when peeked")          void  throwsExceptionWhenPeeked ()  {             assertThrows(EmptyStackException.class, stack::peek);         }         @Nested          @DisplayName("after pushing an element")          class  AfterPushing  {             String  anElement  =  "an element" ;             @BeforeEach              void  pushAnElement ()  {                 stack.push(anElement);             }             @Test              @DisplayName("it is no longer empty")              void  isNotEmpty ()  {                 assertFalse(stack.isEmpty());             }             @Test              @DisplayName("returns the element when popped and is empty")              void  returnElementWhenPopped ()  {                 assertEquals(anElement, stack.pop());                 assertTrue(stack.isEmpty());             }             @Test              @DisplayName("returns the element when peeked but remains not empty")              void  returnElementWhenPeeked ()  {                 assertEquals(anElement, stack.peek());                 assertFalse(stack.isEmpty());             }         }     } }
 
我们可以清晰的看到他们之间的关系
重复测试 @RepeatedTest() 执行多次测试,支持name修改名称(具体见官网,觉得没多大意义),另外可以在方法中获取repetitionInfo参数,用于判断当前的执行情况(JUnit5支持注入参数,后续详说)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j class  RepeatedTestsDemo  {     @RepeatedTest(2)      void  repeatedTest ()  {         log.info("done!" );     }     @RepeatedTest(2)      void  repeatedTest2 (RepetitionInfo repetitionInfo)  {         int  currentRepetition  =  repetitionInfo.getCurrentRepetition();         int  totalRepetitions  =  repetitionInfo.getTotalRepetitions();         log.info(String.format("About to execute repetition %d of %d" ,                  currentRepetition, totalRepetitions));     } }
 
参数测试 @ParameterizedTest 很实用的注解,需要junit-jupiter-params依赖(我们已经添加了)
它主要是配置@xxxSource,注入参数,以完成测试,参数的注入方式有多种
数据源 @ValueSource 注入String内容,这是最常用的
1 2 3 4 5 @ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void  palindromes (String candidate)  {     log.error(candidate); }
 
@EnumSource 注入枚举类
1 2 3 4 5 6 7 8 9 10 11 @ParameterizedTest @EnumSource(TimeUnit.class) void  testWithEnumSource (TimeUnit timeUnit)  {     log.error(timeUnit.toString()); }@ParameterizedTest @EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" }) void  testWithEnumSourceInclude (TimeUnit timeUnit)  {          log.error(timeUnit.toString()); }
 
@MethodSource 通过方法名注入(我更倾向于使用下面的@ArgumentsSource)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @ParameterizedTest @MethodSource("stringProvider") void  testWithExplicitLocalMethodSource (String argument)  {     log.error(argument); }static  Stream<String> stringProvider ()  {     return  Stream.of("apple" , "banana" ); }@ParameterizedTest @MethodSource("stringIntAndListProvider") void  testWithMultiArgMethodSource (String str, int  num, List<String> list)  {          log.error(String.format("Content: %s is %d, %s" , str, num, String.join("," , list))); }static  Stream<Arguments> stringIntAndListProvider ()  {     return  Stream.of(             arguments("apple" , 1 , Arrays.asList("a" , "b" )),             arguments("lemon" , 2 , Arrays.asList("x" , "y" ))     ); }
 
@CsvSource csv源支持
1 2 3 4 5 6 7 8 9 @ParameterizedTest @CsvSource({         "apple,         1",         "banana,        2",         "'lemon, lime', 0xF1" }) void  testWithCsvSource (String fruit, int  rank)  {     log.error(fruit + rank); }
 
它也支持从文件导入,例如@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
@ArgumentsSource 通过自定义的参数提供器导入
1 2 3 4 5 6 7 8 9 10 11 @ParameterizedTest @ArgumentsSource(MyArgumentsProvider.class) void  testWithArgumentsSource (String argument)  {     log.error(argument); }static  class  MyArgumentsProvider  implements  ArgumentsProvider  {     @Override      public  Stream<? extends  Arguments > provideArguments(ExtensionContext context) {         return  Stream.of("apple" , "banana" ).map(Arguments::of);     } }
 
参数转换 为了支持csv,JUnit支持了些内建的转换,详细见文档writing-tests-parameterized-tests-argument-conversion ,如果转换失败,会寻找构造器或者静态构造方法(非私有)中,单String的方法,来转换对应的对象
内建的转换有必要,但后一种,我宁愿得到报错,而不是转换,隐形的转换往往会导致莫名的问题出现
 
所以推荐通过@ConvertWith实现参数类型间的转换
1 2 3 4 5 6 7 8 9 10 11 12 13 @ParameterizedTest @ValueSource(strings = { "Wow,12", "radar,50"}) void  toBook (@ConvertWith(ToBookConverter.class)  Book book)  {     log.error(book.toString()); }static  class  ToBookConverter  extends  SimpleArgumentConverter  {     @Override      protected  Object convert (Object source, Class<?> targetType)  {         String  value  =  String.valueOf(source);         String[] split = value.split("," );         return  Book.of(split[0 ], Integer.parseInt(split[1 ]));     } }
 
JUnit中也内置了些转换,如@JavaTimeConversionPattern等
除外,还可以通过@AggregateWith转换或者接收ArgumentsAccessor对象
Dynamic测试 除了常规的@Test,我们还可以通过@TestFactory来构建整个测试树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 class  DynamicTestsDemo  {     private  final  Calculator  calculator  =  new  Calculator ();          @TestFactory      List<String> dynamicTestsWithInvalidReturnType ()  {         return  Arrays.asList("Hello" );     }     @TestFactory      Collection<DynamicTest> dynamicTestsFromCollection ()  {         return  Arrays.asList(             dynamicTest("1st dynamic test" , () -> assertTrue(isPalindrome("madam" ))),             dynamicTest("2nd dynamic test" , () -> assertEquals(4 , calculator.multiply(2 , 2 )))         );     }     @TestFactory      Iterable<DynamicTest> dynamicTestsFromIterable ()  {         return  Arrays.asList(             dynamicTest("3rd dynamic test" , () -> assertTrue(isPalindrome("madam" ))),             dynamicTest("4th dynamic test" , () -> assertEquals(4 , calculator.multiply(2 , 2 )))         );     }     @TestFactory      Iterator<DynamicTest> dynamicTestsFromIterator ()  {         return  Arrays.asList(             dynamicTest("5th dynamic test" , () -> assertTrue(isPalindrome("madam" ))),             dynamicTest("6th dynamic test" , () -> assertEquals(4 , calculator.multiply(2 , 2 )))         ).iterator();     }     @TestFactory      DynamicTest[] dynamicTestsFromArray() {         return  new  DynamicTest [] {             dynamicTest("7th dynamic test" , () -> assertTrue(isPalindrome("madam" ))),             dynamicTest("8th dynamic test" , () -> assertEquals(4 , calculator.multiply(2 , 2 )))         };     }     @TestFactory      Stream<DynamicTest> dynamicTestsFromStream ()  {         return  Stream.of("racecar" , "radar" , "mom" , "dad" )             .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));     }     @TestFactory      Stream<DynamicTest> dynamicTestsFromIntStream ()  {                  return  IntStream.iterate(0 , n -> n + 2 ).limit(10 )             .mapToObj(n -> dynamicTest("test"  + n, () -> assertTrue(n % 2  == 0 )));     }     @TestFactory      Stream<DynamicTest> generateRandomNumberOfTests ()  {                           Iterator<Integer> inputGenerator = new  Iterator <Integer>() {             Random  random  =  new  Random ();             int  current;             @Override              public  boolean  hasNext ()  {                 current = random.nextInt(100 );                 return  current % 7  != 0 ;             }             @Override              public  Integer next ()  {                 return  current;             }         };                  Function<Integer, String> displayNameGenerator = (input) -> "input:"  + input;                  ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7  != 0 );                  return  DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);     }     @TestFactory      Stream<DynamicNode> dynamicTestsWithContainers ()  {         return  Stream.of("A" , "B" , "C" )             .map(input -> dynamicContainer("Container "  + input, Stream.of(                 dynamicTest("not null" , () -> assertNotNull(input)),                 dynamicContainer("properties" , Stream.of(                     dynamicTest("length > 0" , () -> assertTrue(input.length() > 0 )),                     dynamicTest("not empty" , () -> assertFalse(input.isEmpty()))                 ))             )));     }     @TestFactory      DynamicNode dynamicNodeSingleTest ()  {         return  dynamicTest("'pop' is a palindrome" , () -> assertTrue(isPalindrome("pop" )));     }     @TestFactory      DynamicNode dynamicNodeSingleContainer ()  {         return  dynamicContainer("palindromes" ,             Stream.of("racecar" , "radar" , "mom" , "dad" )                 .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))         ));     } }
 
还未看过源码,但目测@Test是由内建的转换器,转换成DynamicNode,然后再执行。使用@TestFactory,tree型的代码也是种选择,再维护上,不差于@Test的常规方案
扩展 与 Junit4 不同,Junit5 提供了一个统一的一个扩展API。不过在之前,先看下另一个 Junit5 的重要特性–组合注解
组合注解 在官方文档中,这部分与注解部分一同讲的,但我将它移到此处,因为绝大多数情况下,他都是与扩展API一同使用。
组合注解,顾名思义,当一个注解上存在其他的Junit注解时,同时也继承这些注解的语义
例如:组合Tag与Test注解
1 2 3 4 5 6 @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Tag("fast") @Test public  @interface  Fast { }
 
1 2 3 4 @Fast void  asserts ()  {     assertTrue(true ); }
 
Extend API 在 Junit5 中通过 @ExtendWith 注解实现添加扩展。
1 2 3 4 @ExtendWith(DatabaseExtension.class) public  class  SimpleTest  {    }
 
1 2 3 4 5 6 7 8 9 10 11 @Slf4j public  class  DatabaseExtension  implements  BeforeAllCallback , AfterAllCallback {     @Override      public  void  beforeAll (ExtensionContext extensionContext)  throws  Exception {         log.info("连接数据库" );     }     @Override      public  void  afterAll (ExtensionContext extensionContext)  throws  Exception {         log.info("关闭数据库" );     } }
 
@ExtendWith 提供了扩展的入口,具体的实现通过实现对应的接口,例如上面的 DatabaseExtension 实现 BeforeAllCallback,AfterAllCallback
在Junit中,存在许多扩展接口
ExecutionCondition 定义执行条件,满足条件时才能执行,下面是一个例子
1 2 3 4 5 6 7 @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(PassConditionalExtension.class) @Test public  @interface  Pass {     String value () ; }
 
1 2 3 4 5 6 7 8 9 10 public  class  PassConditionalExtension  implements  ExecutionCondition  {     @Override      public  ConditionEvaluationResult evaluateExecutionCondition (ExtensionContext context)  {         return  AnnotationUtils.findAnnotation(context.getElement(), Pass.class)                 .map(Pass::value)                 .filter("我很帅" ::equals)                 .map(item -> ConditionEvaluationResult.enabled("pass" ))                 .orElse(ConditionEvaluationResult.disabled("pass is not okay!" ));     } }
 
1 2 3 4 5 6 7 8 9 10 public  class  ConditionalTest  {     @Pass("密码不对不执行")      void  notExec ()  {              }     @Pass("我很帅")      void  exec ()  {              } }
 
TestInstanceFactory 定义测试实例,只能用于class上,暂时想不到例子,跳过~~
TestInstancePostProcessor 对测试实例处理,通常用于注入依赖,暂时想不到例子,跳过~~
TestInstancePreDestroyCallback 当测试实例销毁前调用,暂时想不到例子,跳过~~
ParameterResolver 处理参数,见下面例子
1 2 3 4 5 6 @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public  @interface  BookInject {     String title () ;     int  price ()  default  0 ; }
 
1 2 3 4 5 6 7 8 9 10 11 12 public  class  BookParameterResolver  implements  ParameterResolver  {     @Override      public  boolean  supportsParameter (ParameterContext parameterContext, ExtensionContext extensionContext)  throws  ParameterResolutionException {         return  parameterContext.isAnnotated(BookInject.class);     }     @Override      public  Object resolveParameter (ParameterContext parameterContext, ExtensionContext extensionContext)  throws  ParameterResolutionException {         return  parameterContext.findAnnotation(BookInject.class)                 .map(book -> Book.of(book.title(), book.price()))                 .orElse(null );     } }
 
1 2 3 4 5 6 7 8 @Slf4j public  class  BookParameterTest  {     @Test      @ExtendWith(BookParameterResolver.class)      void  exec (@BookInject(title = "删库")  Book book)  {         log.info(book.toString());     } }
 
TestWatcher 监听测试用例的执行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Slf4j public  class  LogTestWatcher  implements  TestWatcher  {     @Override      public  void  testSuccessful (ExtensionContext context)  {         log.info("wow, 成功了!" );     }     @Override      public  void  testAborted (ExtensionContext context, Throwable cause)  {              }     @Override      public  void  testDisabled (ExtensionContext context, Optional<String> reason)  {              }     @Override      public  void  testFailed (ExtensionContext context, Throwable cause)  {              } }
 
生命周期回调 在一开始的例子中就是生命周期的回调,这里不写例子拉,他们执行的先后顺序如下
BeforeAllCallback
BeforeEachCallback
BeforeTestExecutionCallback 
AfterTestExecutionCallback 
 
 
AfterEachCallback 
 
 
AfterAllCallback 
 
TestExecutionExceptionHandler 处理异常,如果存在一些自定义的运行时异常,这是很有用的,可以做些处理
1 2 3 4 5 6 7 8 9 public  class  IgnoreExceptionExtension  implements  TestExecutionExceptionHandler  {     @Override      public  void  handleTestExecutionException (ExtensionContext context, Throwable throwable)  throws  Throwable {         if  (throwable instanceof  Exception) {             return ;         }         throw  throwable;     } }
 
1 2 3 4 5 6 7 8 9 10 11 12 public  class  SimpleTest  {     @Test      @ExtendWith(IgnoreExceptionExtension.class)      void  exec2 ()  throws  Exception {         throw  new  Exception ("被忽略" );     }     @Test      @ExtendWith(IgnoreExceptionExtension.class)      void  exec3 ()  throws  Throwable {         throw  new  Throwable ("不被忽略" );     } }
 
Intercepting Invocations 拦截测试方法,类似于 Spring 中的 AOP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Slf4j @ExtendWith(MyInvocationInterceptorTest.LogInvocationInterceptor.class) public  class  MyInvocationInterceptorTest  {     @ParameterizedTest      @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })      void  showParameterized (String candidate)  {         log.error(candidate);     }     static  class  LogInvocationInterceptor  implements  InvocationInterceptor  {         @Override          public  void  interceptTestTemplateMethod (Invocation<Void> invocation,                                                 ReflectiveInvocationContext<Method> invocationContext,                                                 ExtensionContext extensionContext)  throws  Throwable {             Method  executable  =  invocationContext.getExecutable();             List<Object> arguments = invocationContext.getArguments();             Class<?> targetClass = invocationContext.getTargetClass();             log.info("executable method: "  + executable.getName());             log.info("arguments: "  + arguments.stream().map(String::valueOf).collect(Collectors.joining()));             log.info("targetClass: "  + targetClass.getName());             log.info("invocation.proceed() start" );             invocation.proceed();             log.info("invocation.proceed() end" );         }     } }
 
InvocationInterceptor 中有多个方法 interceptBeforeAllMethod interceptTestMethod interceptTestTemplateMethod 等,分别在不同的时候拦截,里中 @ParameterizedTest 继承 @TestTemplate 所以使用 interceptTestTemplateMethod
拦截器中一般会传入这几个变量:
invocation: 测试请求,只有proceed()代表执行 
invocationContext: 测试请求的上下文 
extensionContext: 扩展的上下文 
 
为 Test Templates 提供上下文 上面提到了 @ParameterizedTest 是由 @TestTemplate, 而 @TestTemplate 至少需要一个 TestTemplateInvocationContextProvider 提供时执行,在 @ParameterizedTest 中我们可以看到,@ParameterizedTest 由 ParameterizedTestExtension.class 提供测试的参数
1 2 3 4 5 @TestTemplate @ExtendWith(ParameterizedTestExtension.class) public  @interface  ParameterizedTest {    }
 
所以,相对于我写例子,直接学习它的源码可能更好,这是真实的案例,下面是 ParameterizedTestExtension.class 部分内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class  ParameterizedTestExtension  implements  TestTemplateInvocationContextProvider  {   private  static  final  String  METHOD_CONTEXT_KEY  =  "context" ;         @Override    public  boolean  supportsTestTemplate (ExtensionContext context)  {     if  (!context.getTestMethod().isPresent()) {       return  false ;     }     Method  testMethod  =  context.getTestMethod().get();     if  (!isAnnotated(testMethod, ParameterizedTest.class)) {       return  false ;     }     ParameterizedTestMethodContext  methodContext  =  new  ParameterizedTestMethodContext (testMethod);     Preconditions.condition(methodContext.hasPotentiallyValidSignature(),       () -> String.format(         "@ParameterizedTest method [%s] declares formal parameters in an invalid order: "              + "argument aggregators must be declared after any indexed arguments "              + "and before any arguments resolved by another ParameterResolver." ,         testMethod.toGenericString()));     getStore(context).put(METHOD_CONTEXT_KEY, methodContext);     return  true ;   }               @Override    public  Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts (ExtensionContext extensionContext)  {     Method  templateMethod  =  extensionContext.getRequiredTestMethod();     String  displayName  =  extensionContext.getDisplayName();     ParameterizedTestMethodContext  methodContext  =  getStore(extensionContext)         .get(METHOD_CONTEXT_KEY, ParameterizedTestMethodContext.class);     ParameterizedTestNameFormatter  formatter  =  createNameFormatter(templateMethod, displayName);     AtomicLong  invocationCount  =  new  AtomicLong (0 );          return  findRepeatableAnnotations(templateMethod, ArgumentsSource.class)         .stream()         .map(ArgumentsSource::value)         .map(this ::instantiateArgumentsProvider)         .map(provider -> AnnotationConsumerInitializer.initialize(templateMethod, provider))         .flatMap(provider -> arguments(provider, extensionContext))         .map(Arguments::get)         .map(arguments -> consumedArguments(arguments, methodContext))         .map(arguments -> createInvocationContext(formatter, methodContext, arguments))         .peek(invocationContext -> invocationCount.incrementAndGet())         .onClose(() ->             Preconditions.condition(invocationCount.get() > 0 ,                 "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest" ));        }    }
 
在扩展中保持状态 熟悉前端的知道在 vue 或者 react 中都会涉及到状态 state 的保持,在junit 5 中也提供了类似的API Store (连名字都差不多。。。),大致上你可以理解为Map这类的东西,在 ParameterizedTestExtension 中也使用它存储了 METHOD_CONTEXT_KEY
在 Spring 中的使用 
未完待续
 
最后 一个疑问,JUnit5 的注解风格和 Spring 为何如此接近。。。