关于JUnit 5扩展的综合指南

771 阅读23分钟

JUnit是Java生态系统中最流行的单元测试框架之一。JUnit 5版本(也被称为Jupiter)包含许多令人兴奋的创新,包括支持Java 8及以上版本的新功能。 然而,许多开发人员仍然喜欢使用JUnit 4框架,因为某些功能如JUnit 5的并行执行仍处于实验阶段。

撇开一些小事不谈,JUnit 5仍然代表着测试框架发展的一个重要步骤,因为它提供了先进的注释,让你可以测试反应式应用程序。根据我的经验,JUnit 5是目前最好的JUnit版本。新的框架还带来了一个可扩展的架构和一个全新的扩展模型,使得实现自定义功能变得非常容易。

JUnit 5 Extensions

资料来源

在这个JUnit教程中,我们深入探讨了JUnit 5的扩展--JUnit 5框架的主要功能之一。

JUnit 5有什么了不起的地方?

如果你使用过JUnit 4框架,你会同意,扩展或定制JUnit 4框架的可能性已经减少(或极少)。这是该版本的JUnit框架的最大瓶颈之一。在JUnit 4中,可以通过简单地用@RunWith(MyRunner.class)注释测试类来创建Runners这样的扩展,这样JUnit就可以使用它们。

这种方法的缺点是,你对一个测试类只使用一个Runner。这使得它很难与多个运行器进行组合。然而,用JUnit 4的Runner所带来的缺点可以用以下选项来克服。

  • JUnit 4在Runners之外还使用了Rules,为你提供了一个灵活的解决方案来添加或重新定义每个测试方法的行为。
  • 可以创建规则来注释测试类的字段。然而,Rules有一个不变性问题。简单地说,Rules只能在测试运行之前和之后执行,但不能在测试中实现。

那么,JUnit 5框架是如何解决JUnit 4这个挥之不去的问题的?JUnit 5提供了一个扩展机制,通过扩展模型打开第三方工具或API。它由一个单一而连贯的扩展API概念组成,以克服JUnit 4的扩展点(即Runner、TestRule和MethodRule)竞争的局限性。

现在我们已经涵盖了关于JUnit 5扩展的要点,下面是Java开发人员立即冒出来的一系列问题。

  • 我们为什么要使用扩展?
  • 使用JUnit 5扩展需要多少努力?
  • 扩展模型是否比 "编程模型 "更好?

下面是JUnit 5的核心原则中提到的内容。

通过创建或增强一个扩展点来实现新功能,比把功能作为核心功能来添加要好。

JUnit 5架构

JUnit框架的前几个版本(即直到JUnit 4)是以单个jar形式交付的。然而,JUnit 5在架构上与早期的JUnit版本不同。因此,JUnit 5以不同的模块交付,以满足将API、执行引擎、执行和集成分开的新架构。

JUnit 5只能用于大于或等于8的Java版本。下面是构成JUnit 5框架的三个模块。

  1. JUnit平台。为发现和运行测试的工具提供一个API。它定义了JUnit与希望从IDE、构建工具或控制台运行测试的客户之间的接口。
  2. JUnit Jupiter。提供了一个基于注解的API来编写JUnit 5单元测试,以及一个让你运行它们的测试引擎。
  3. JUnit Vintage。提供一个测试引擎来运行JUnit 3和JUnit 4测试,从而确保向后兼容(与JUnit框架的早期版本)。

这个架构的目标是分离测试、执行和扩展的责任。它也有利于其他测试框架与JUnit框架的整合。

JUnit 5 Architecture

编程模型VS.扩展模型

如果你是一个经常写测试的QA工程师,你肯定会使用编程模型。另一方面,扩展模型提供了几个接口作为扩展API,可由扩展提供者(开发者或工具供应商)实现,以扩展JUnit 5的核心功能。

JUnit 5 Architecture

JUnit 5架构

从上图所示的JUnit 5架构中可以看出,扩展模型是Jupiter模块的一部分,可以让你通过灵活而强大的扩展来扩展JUnit 5的核心功能。此外,JUnit 5扩展克服了JUnit 4扩展的局限性,取代了Runners和Rules,即其竞争的扩展机制。最后,由于JUnit 5提供了向后兼容性,你仍然可以用JUnit 5运行JUnit 4测试

JUnit Jupiter的扩展模型通过org.junit.jupiter.api.extension包中的一个小接口暴露出来,开发者或扩展提供者可以使用。

现在我们已经涵盖了JUnit 5扩展的要点,让我们用代码来说明一个JUnit 5扩展的例子。为此,让我们使用Eclipse IDE创建一个Java项目,在一个Java类中有三个测试用例。

JUnit 5 Extensions

JUnit 5 Extensions

如果你熟悉其他的Java IDE(除了Eclipse),你可以查看我们的详细博客,深入了解如何在Eclipse IDE中运行JUnit。将JUnit 5库添加到构建路径(或为Maven项目添加依赖项)后,我们看到JUnit 5扩展在org.junit.jupiter.api中,如下图所示。

这里有一个Java实现的样本,展示了一个简单的JUnit 5扩展的例子。

class FirstTestCase {
 
    @BeforeAll
    static void setUpBeforeClass() throws Exception {
    }
    @AfterAll
    static void tearDownAfterClass() throws Exception {
    }
    @BeforeEach
    void setUp() throws Exception {
    }
    @AfterEach
    void tearDown() throws Exception {
    }
    @Test
    void test() {
        fail("Not yet implemented");
    }
}

从上面的实现中可以看出,我们使用了与测试执行生命周期有关的JUnit注解,我们将在以后的时间点上讨论。

LambdaTest为Java开发人员提供了免费的JUnit认证,这将有助于加速你在Java开发和测试方面的职业生涯。从LambdaTest的JUnit认证中可以略知一二。

如何注册JUnit 5的扩展

JUnit 5中的扩展注册是通过Java的ServiceLoader机制来注册一个或多个扩展。有三种注册扩展的方式。声明式、程序式和自动式。

一个或多个扩展的注册可以使用测试接口、测试类(或其字段)或测试方法上的注解来完成,这取决于注册的类型。

  • 声明式注册。@ExtendWith(classReference.class)注解
    ,应该用于将扩展应用于类的字段、测试接口、测试方法或自定义组成的注解中

    // 1. For Test Class
    @ExtendWith(LoggingExtension.class)
    // 2. Composed Annotation
    @ExtendWith({LoggingExtension.class, DivideExceptionHandler.class})
    public class RegisteringExtensionTest {
        
        // 3. For Test Method
        @ExtendWith(DivideExceptionHandler.class)
        @Test
        void divideTestMethod() {
            Calculate.divide(5, 0);
        }
        @ExtendWith(LoggingExtension.class)
        @Test
        void divideMethod() {
            Calculate.divide(0, 0);
        }
    }
    

    为了用一个JUnit 5扩展的例子来证明这一点,我们使用了一个显示处理测试结果异常的样本。

    public class DivideExceptionHandler implements TestExecutionExceptionHandler{
        @Override
        public void handleTestExecutionException(ExtensionContext ctx, Throwable throwable) throws Throwable {
            // handle exception 
            System.out.println("operation not allowed for division");
        }
    }
    

    我们使用了@ExtendWith (AdditionalOutputExtension.class)注解来注册上述类,这样JUnit框架就可以在后期使用它。

    @ExtendWith(AdditionalOutputExtension.class)
    public class ArithmeticTest { 
        private int result = 5;
     
        @ExtendWith(DivideExceptionHandler.class)
        @Test
        void test_Divide_by_zero() {
          result = Calculate.divide(result, 0);
          System.out.println("test_Divide(5,0) => "+result);
        } 
    }
    
  • 编程式注册。我们可以通过将它们应用于测试类中的字段来使用@RegisterExtension注解。

public class WebServerDemo {
    @RegisterExtension 
    static WebServerExtension server = WebServerExtension.builder()
                                        .enableSecurity(false) 
                                        .build();
 
    @Test 
    void getProductList() { 
    WebClient webClient = new WebClient();
    String serverUrl = server.getServerUrl();
     // Use WebClient to connect to web server using serverUrl and verify response 
    assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
     } 
}
  • 自动注册。我们可以使用java.util.ServiceLoader来自动检测和注册第三方扩展。

JUnit 5有条件的测试执行与注解

对于初学者来说,条件测试执行允许通过org.junit.jupiter.api.condition API根据某些条件来运行(启用)或跳过(禁用)测试案例。让我们看看条件包的注解如何在JUnit 5中用于实现条件测试执行。

1.操作系统条件

操作系统条件可与@EnabledOnOs和@DisabledOnOs注解一起使用。这些条件有助于在特定的平台(或操作系统)上运行JUnit 5测试。

public class OsConditionalTest {
    
  @Test
  @EnabledOnOs(OS.MAC)
  void runOnlyOnMacOs() {
      System.out.println("Run the batch job only on MAC OS");
  }
  @Test
  @EnabledOnOs({ OS.LINUX, OS.MAC })
  void runOnlyOnLinuxOrMac() {
    System.out.println("Run the batch job only on LINUX or MAC OS");
  }
  @Test
  @DisabledOnOs(OS.WINDOWS)
  void notRunOnWindows() {
    System.out.println("Not run the batch job on WINDOWS OS");
  }
  
  @Test
  @EnabledOnOs({ OS.WINDOWS })
  void runOnlyOnWindows() {
    System.out.println("Run the batch job only on WINDOWS OS");
  }
  
  @Test
    @DisabledOnOs({OS.AIX, OS.LINUX, OS.SOLARIS})
    void notRunOnAIXorLinuxOrSolaris() {
        System.out.println("Not run the batch job on AIX or LINUX or SOLARIS");
    } 
}

2.Java运行时环境条件

使用@EnabledOnJre、@DisabledOnJre和@EnabledForJreRange注解,测试用例可以在与JRE(Java Runtime Environment)相关的特定条件下运行,或在JRE版本的特定范围内运行。

public class JreConditionalTest {
    
      @Test
      @EnabledOnJre(JRE.JAVA_8)
      void runOnlyOnJava8() {
          System.out.println("Run the compatibility test only on JRE 8");
      }
      @Test
      @EnabledOnJre({JRE.JAVA_13, JRE.JAVA_14})
      void runOnlyOnJava13OrJava14() {
        System.out.println("Run the compatibility test only on JRE 13 and JRE 14");
      }
      @Test
      @DisabledOnJre(JRE.JAVA_13)
      void notRunOnJava13() {
        System.out.println("not run the compatibility test on JRE 13");
      }
      
      @Test
      @EnabledOnJre(JRE.JAVA_11)
      void runOnlyOnJava11() {
        System.out.println("Run the compatibility test only on JRE 11");
      }
      
      @Test
      @DisabledOnJre({JRE.JAVA_10, JRE.JAVA_11})
        void notRunOnJava10andJava11() {
        System.out.println("not Run the compatibility test on JRE 10 and JRE 11");
        }  
}

3.系统属性条件

可以使用@EnabledIfSystemProperty和/或@DisabledIfSystemProperty注解根据系统属性启用或禁用测试案例。

public class SystemPropertyConditionalTest {
    @Disabled
    @Test
    void printSystemProperties() {
      //remove @Disabled to see System properties
      System.getProperties().forEach((key, value) -> System.out.println(key+" - "+value));
    }
    @Test
    @EnabledIfSystemProperty(named = "java.vm.vendor", matches = "Oracle.*")
    void runOnlyOnOracleJDK() {
      System.out.println("Run this only on Oracle JDK");
    }
    @Test
    @EnabledIfSystemProperty(named = "os.arch", matches = ".*32.*")
    void runOnlyOn32bitOS() {
      System.out.println("Run this on only on 32 bit OS");
    }
    
    @Test
    @DisabledIfSystemProperty(named = "os.version", matches = ".*10.*")
    void notRunOnlyOnWindows10() {
      System.out.println("not run this only on windows 10 version");
    }
    
    @Test
    @EnabledIfSystemProperty(named = "os.version", matches = ".*10.*")
    void runOnlyOnWindows10() {
      System.out.println("Run this only on WINDOWS OS 10 version");
    }
}

4.环境变量条件

JUnit 5测试案例可以根据环境变量的条件(或值)来启用或禁用。这可以使用JUnit 5框架中的@EnabledIfEnvironmentVariable和@DisabledIfEnvironmentVariable注解来完成。

public class EnvironmentVariableConditionalTest {
    @Disabled
    @Test
    void printSystemProperties() {
      // Remove @Disabled to see environment properties
      System.getenv().forEach((key, value) -> System.out.println(key+" - "+value));
    }
    @Test
    @EnabledIfEnvironmentVariable(named = "COMPUTERNAME", matches = "sysname")
    void runOnlyOnPerticularMachine() {
      System.out.println("Run this only on particular server");
    }
    @Test
    @DisabledIfEnvironmentVariable(named = "PROCESSOR_ARCHITECTURE", matches = ".*32.*")
    void noRrunOn32bitOS() {
      System.out.println("Not run this on 32 bit OS");
    }
    
    @Test
    @EnabledIfEnvironmentVariable(named = "USERNAME", matches = "username")
    void runOnlyForParticularUser() {
      System.out.println("run this only for particular user in system");
    }
}

5.自定义条件

自定义条件可以通过ExecutionCondition扩展API来设置启用或禁用测试案例。下面是两种方式,通过它们可以实现在特定(自定义)条件下运行的测试用例。

  • 结合内置注解,创建一个自定义注解,以后可作为测试条件使用。

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Test
    @EnabledOnOs({ OS.WINDOWS })
    @EnabledIfSystemProperty(named = "os.version", matches = ".*10.*")
    @EnabledIfEnvironmentVariable(named = "PROCESSOR_ARCHITECTURE", matches = ".*64.*")
    public @interface RunOnlyOn64bitWindows10 {
     
    }
    

    测试条件里面的组合内置注解可以作为测试类里面的注解来使用。它将有助于定义测试应该在哪些条件下执行。

    public class CustomBuiltInTest {
        @RunOnlyOn64bitWindows10
        void runOnlyOn64bitWindows10() {
            System.out.println("Run only this on 64-bit Windows 10 System.");
          }
    }
    
  • 自定义注解可以使用ExecutionCondition扩展API从头开始创建。使用这种方法,你可以不使用内置注解。为了演示使用JUnit 5扩展例子的自定义注解,我们在运行环境(即环境可以是开发、QA或生产)的条件下运行测试,如下所示:

    public class EnvironmentConditionalTests {
        @Test
        @Environment(enabledFor = {"Dev", "QA"})
        void add() {
            Assertions.assertEquals(2, Calculate.add(1, 1));
        }
        @Test
        void multiply () {
            Assertions.assertEquals(6, Calculate.multiple(3, 2));
        }
    }
    

    这里,运行添加()测试的条件是在测试或开发环境中执行的(不是实时的)。下面是如何从头开始创建@Environment注解,并在JUnit 5扩展示例中实现它。

    • 我们创建一个Environment.java文件,并设置enabledFor属性,在其上添加参数。接下来,创建的注解必须通过EnvironmentExecutionCondition文件使用@ExtendWith注解注册条件扩展。
    • 创建EnvironmentExecutionCondition文件,所有的条件都将在ExecutionCondition API的实现上被指定。
@ExtendWith(EnvironmentExecutionCondition.class)
@Retention(RUNTIME)
public @interface Environment {
    String[] enabledFor();
}
public class EnvironmentExecutionCondition implements ExecutionCondition{
 
    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context){
        String activeEnvironment = System.getProperty("environment");
        if(activeEnvironment == null) {
            return ConditionEvaluationResult.disabled("There is no active environment");
        }
        Set<String> enabledEnvironments = getEnabledEnvironment(context);
        return enabledEnvironments.contains(activeEnvironment)
            ? ConditionEvaluationResult.enabled("active environment is enabled")
            : ConditionEvaluationResult.disabled("active environment is not enabled");
    }
 
    private Set<String> getEnabledEnvironment(ExtensionContext context) {
        Set<String> enabledEnvironments = new HashSet<>();
      context.getElement().ifPresent(element ->
 AnnotationSupport.findAnnotation(element, Environment.class)
                .map(Environment::enabledFor)
                .ifPresent(array -> enabledEnvironments.addAll(Arrays.asList(array)))
                );
        return enabledEnvironments;
    }       
}

当在开发或QA环境中运行测试时,"添加 "测试将被激活并执行,而如果你在Prod环境中,测试将不会运行。

要在一个给定的环境中执行测试,在 "运行配置 "参数下的VM参数上运行适当的命令。

  1. 开发环境:-ea -Denvironment=Dev
  2. QA环境: -ea -Denvironment=QA
  3. 开发(或实时)环境: -ea -Denvironment=live

阅读 - 如何在命令行中运行Junit测试

如何通过实现TestInstanceFactory来创建JUnit 5扩展?

我们可以通过实现TestInstanceFactory API来创建JUnit 5扩展,以创建测试类实例。这些应该在每个测试方法的执行之前运行。

然后,创建的测试实例可以从依赖注入框架获得,或者通过调用静态工厂方法来创建。

下面的JUnit 5扩展例子演示了测试实例工厂在外部和内部类上的使用。

@ExtendWith(CustomTestInstanceFactory.class)
public class OuterTest {
    
    @Test
    void outer() {
    }


    @Nested
    // @ExtendWith(CustomTestInstanceFactory.class)
    class Inner {
 
        @Test
        void inner() {
        }
 
        @Nested
        // @ExtendWith(CustomTestInstanceFactory.class)
        class InnerInner {
 
            @Test
            void innerInner() {
            }
        }
    }
}
import static org.junit.platform.commons.util.ReflectionUtils.newInstance;
 
public class CustomTestInstanceFactory implements TestInstanceFactory{
 
    public Object createTestInstance(TestInstanceFactoryContext factoryContext, ExtensionContext extensionContext)
            throws TestInstantiationException {
    
        try {
            Optional<Object> outerInstance = factoryContext.getOuterInstance();
            Class<?> testClass = factoryContext.getTestClass();
            if (outerInstance.isPresent()) {
                System.out.println("createTestInstance() called for inner class: " 
                                     + testClass.getSimpleName());
                 return newInstance(testClass, outerInstance.get());
            }
            else {
                System.out.println("createTestInstance() called for outer class: "
                                      + testClass.getSimpleName());
                 return newInstance(testClass);
            }
        }
        catch (Exception e) {
            throw new TestInstantiationException(e.getMessage(), e);
        }
        
    }
}

如何在JUnit 5中测试生命周期回调

生命周期回调是在某些模型方法之前或之后自动执行的函数。例如,你可以使用生命周期回调,在创建或更新用户记录之前自动计算 "全名 "属性的值。

生命周期方法和测试实例生命周期

在主要的测试实例生命周期中,JUnit 5定义了类和方法的生命周期,由以下注解驱动。

  1. @BeforeAll
  2. @BeforeEach
  3. @AfterEach
  4. @AfterAll

用@BeforeAll和@AfterAll注解的方法应该在类的所有测试方法之前和之后执行。另一方面,由@BeforeEach和@AfterEach注解的方法应该在每个测试方法之前和之后分别执行。

JUnit在测试实例生命周期中运行每个测试之前为测试类创建一个新实例。这种行为旨在单独运行每个测试,从而避免运行其他测试带来的副作用。

class TestInstanceLifecycle {
 
    public TestInstanceLifecycle() {
        super();
        System.out.println("test instance Constructor");
    }
 
    @BeforeAll
    static void setUpBeforeClass() throws Exception {
        System.out.println("@BeforeAll : Before the entire test fixture");
    }
 
    @AfterAll
    static void tearDownAfterClass() throws Exception {
         System.out.println("@AfterAll : After the entire test fixture");
    }
    @BeforeEach
    void setUp() throws Exception {
         System.out.println("@BeforeEach : Before each test");
    }
    @AfterEach
    void tearDown() throws Exception {
        System.out.println("@AfterEach : After each test");
    }
    @Test
    void firstTest() {
        System.out.println("First test");
    }   
    @Test
    void secondTest() {
        System.out.println("Second test");
    }
 
}

上面的执行得到了以下结果。

@BeforeAll: Before the entire test fixture
test instance Constructor
   @BeforeEach: Before each test
	First test
   @AfterEach: After each test
            test instance Constructor
    @BeforeEach: Before each test
	Second test
    @AfterEach: After each test
@AfterAll: After the entire test fixture

从测试执行结果来看,默认行为是 "每个方法生命周期"。

Per Method Lifecycle

测试生命周期的默认行为可以使用@org.junit.jupiter.api.TestInstance API来改变,它允许改变默认的生命周期(对于一个测试类或一个测试方法)。这可以通过向测试类添加@TestInstance(TestInstance.Lifecycle.PER_CLASS)注解来实现。

这里是修改默认行为(测试生命周期)后更新的执行结果。

test instance Constructor
@BeforeAll: Before the entire test fixture
@BeforeEach: Before each test
	First test
@AfterEach: After each test
@BeforeEach: Before each test
	Second test
@AfterEach: After each test
@AfterAll: After the entire test fixture

从测试执行结果来看,修改后的行为给出了 "Per Class Lifecycle"。

Per Class Lifecycle

JUnit 5扩展生命周期

除了每个类和每个方法的生命周期,JUnit 5 Jupiter还提供了不同的接口,定义了在执行生命周期的不同点上扩展测试的API。因此,JUnit 5调用扩展回调来实现该行为。

这些API是org.junit.jupiter.api.extension包的一部分。下面是定义扩展生命周期的API。

  • AfterAllCallback
  • AfterEachCallback
  • BeforeAllCallback
  • BeforeEachCallback

我们可以通过实现BeforeAllCallback、AfterAllCallback、BeforeEachCallback和AfterEachCallback接口,创建一个应用于测试类的扩展。

public class ExtensionCallbackLifecycle implements BeforeAllCallback, AfterAllCallback, 
BeforeEachCallback, AfterEachCallback {
 
    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        System.out.println("After Each from AfterEachCallback Extension");
    }
 
    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        System.out.println("Before Each from BeforeEachCallback Extension");
    }
 
    @Override
    public void afterAll(ExtensionContext context) throws Exception {
        System.out.println("After All from AfterAllCallback Extension");
    }
 
    @Override
    public void beforeAll(ExtensionContext context) throws Exception {
        System.out.println("Before All from BeforeAllCallback Extension");
    }
 
}

下面是如何将上述扩展点应用于一个测试类。

@ExtendWith(ExtensionCallbackLifecycle.class)
public class ExtensionLifecycleTest {
 
    public ExtensionLifecycleTest() {
            super();
           System.out.println("Test instance constructor");
    }
    @BeforeEach
    void beforeEachTest() {
        System.out.println("Before each test");
     }
 
    @AfterEach
    void afterEachTest() {
        System.out.println("After each test");
     }
 
    @Test
    void firstTest() {
        System.out.println("First test");
    }
 
    @Test
    void secondTest() {
        System.out.println("Second test");
    }
}

下面是执行结果。

Before All from BeforeAllCallback Extension
Test instance constructor
Before Each from BeforeEachCallback Extension
Before each test
First test
After each test
After Each from AfterEachCallback Extension
Test instance constructor
Before Each from BeforeEachCallback Extension
Before each test
Second test
After each test
After Each from AfterEachCallback Extension
After All, from AfterAllCallback Extension

JUnit 5中的测试实例后处理

Juniper扩展模型通过实现TestInstancePostProcessor接口,提供了在创建测试实例后对测试实例进行后处理的能力。按照测试实例工厂,它可以通过使用例如注入依赖关系到实例中来调用测试实例的初始化方法,以使用测试实例的后处理。

为了说明这一点,我们以log4j API中的日志系统为例,它在每次测试执行后执行并写入日志。让我们在这个JUnit 5异常例子中进一步检查细节。

public class LoggingPostProcessExtension implements TestInstancePostProcessor{
 
  @Override
  public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
        Logger logger = LogManager.getLogger(testInstance.getClass()
                                                         .getName());
System.out.println("Test instance Post-          Process Extension called on :"+ testInstance.getClass().getName());
        testInstance.getClass()
                    .getMethod("createLogger", Logger.class)
                    .invoke(testInstance, logger); 
    }   
}
public class ArithmeticTest {
    private int result = 5;


    @ExtendWith(LoggingPostProcessExtension.class)
    @Test
    void test_Divide() {
      result = Calculate.divide(result, 5);
      System.out.println("test_Divide(5,5) => "+ result);
      Assertions.assertEquals(1, result);
    }

JUnit 5中的测试实例预销毁回调

扩展模型也定义了在测试实例和最终销毁之间需要处理的扩展的API。例如,测试实例预销毁回调通常用于在测试实例中使用后的依赖性注入清理等情况。

public class DisplayPredestroyedInstances implements  TestInstancePreDestroyCallback{
 
    @Override
    public void preDestroyTestInstance(ExtensionContext ctx) throws Exception {
        
          List<Object> destroyedInstances =
                    new ArrayList<>(context.getRequiredTestInstances().getAllInstances());
            for (Optional<ExtensionContext> current = context.getParent(); 
                    current.isPresent(); 
                    current = current.get().getParent()) {
                current.get().getTestInstances()
                             .map(TestInstances::getAllInstances)
                             .ifPresent(destroyedInstances::removeAll);
            }
            Collections.reverse(destroyedInstances);
            destroyedInstances.forEach(testInstance -> 
            System.out.println("preDestroy: " + testInstance));
    }
}
public class ArithmeticTest {
    
    private int result = 5;
      @ExtendWith(DisplayPredestroyedInstances.class)
      @Test
      void test_Multiply() {
        result = Calculate.multiple(result, 5);
        System.out.println("test_Multiply(5,5) => "+ result);
        Assertions.assertEquals(25, result);
      }
}

JUnit 5中的参数解析

大多数的测试方法没有参数。当使用参数时,我们使用ParameterResolver接口,它定义了用于扩展的API org.junit.jupiter.api.extension.ParameterResolver。它提供了在运行时动态地解决参数的功能。

然后,测试类的以下构造函数和注释方法可以有一个或多个参数。

  1. @测试
  2. @TestFactory
  3. @BeforeEach
  4. @AfterEach
  5. @BeforeAll
  6. @AfterAll

参数解析可以通过名称、类型、注解或相同的组合来进行。JUnit 5使用测试类的构造函数和方法的参数实现了依赖性注入,使之成为可能。

这些参数必须在运行时由需要事先注册的ParameterResolver类型的实例解决。

默认情况下,JUnit 5使用三个内置的解析器自动注册ParameterResolver。

  • TestInfoParameterResolver。用于解析、注入TestInfo类型的实例,并获得正在执行的测试的信息。
  • RepetitionInfoParameterResolver。用于注入RepetitionInfo类型的实例,仅用于重复测试。
  • TestReporterParameterResolver。用于注入一个TestReporter类型的实例,允许它在测试报告中添加有用的信息。

如果你使用的是JUnit 4,你可以查看我们的详细博客,深入了解Selenium自动化的JUnit的参数化

public class BuiltInParamResolver {
    @Test
    @DisplayName("TestInfo Param Resolver")
    void firstTestCase(TestInfo testInfo) {
        assertEquals("TestInfo Param Resolver", testInfo.getDisplayName());
        System.out.println("TestInfo executed !");
    }
    
     @RepeatedTest(3)
     @DisplayName("RepetitionInfo Param Resolver")
      void test_repeted(RepetitionInfo repetitionInfo) {
        System.out.println("start test_repeted : "+repetitionInfo.getCurrentRepetition());
        assertEquals(9, Calculate.add(5, 4));
      }
    
    @Test
    @DisplayName("Testreport Param Resolver")
    void testReport(TestReporter testReporter) {
       testReporter.publishEntry("test reporter with single value : "+Calculate.add(4, 3));
        assertEquals(7, Calculate.add(4, 3));
    }
}

JUnit 5中的异常处理

TestExecutionExceptionHandler接口定义了实现扩展的API,让你在抛出异常时完全定制测试案例的行为。

为了延续先前的JUnit 5扩展例子,我们在divide测试用例上使用了ArithmeticException来创建一个测试类,如下所示。

public class ArithmeticTest {
    
    private int result = 5;
 
    @ExtendWith(DivideExceptionHandler.class)
    @Test
    void test_Divide_by_zero() {
      result = Calculate.divide(result, 0);
      System.out.println("test_Divide(5,0) => "+ result);
    } 
}

它被扩展为一个异常处理类,用于处理被除法操作抛出的异常(当处理除以0时)。

public class DivideExceptionHandler implements TestExecutionExceptionHandler{
 
    @Override
    public void handleTestExecutionException(ExtensionContext ctx, Throwable throwable) 
throws Throwable {
         // handle exception 
        System.out.println("operation not allowed for division");
    }
}

可以使用传统的抛出异常的方法(使用try...catch,Rules等),也可以通过实现TestExecutionExceptionHandler接口的注解。

阅读 -用JUnit断言掌握Selenium测试

JUnit 5中的第三方框架扩展

JUnit背后的原则是提供一个容易扩展的基本框架,使用户的行动比API开发者更快。这一特点使得建立作为第三方库基础的API成为可能。

尽管JUnit 5有许多第三方扩展,但我们将介绍以下扩展,因为它们被开发者社区广泛使用。

  • MockitoExtension
  • Selenium-Jupiter
  • Spring TestContext。Jupiter的SpringExtension

1.MockitoExtension

JUnit 5最适合于运行单元测试。然而,当执行模块(或相互依赖的资源)之间的集成测试和交互验证时,存根或模拟被用来模拟(或代表)依赖或不可用的资源。Mockito是一个框架,允许创建模拟对象进行集成测试。

以下是你可以使用MockitoExtension的主要方式。

  1. 手动方法
  2. 使用注解
  3. 使用在mockito-junit-jupiter工件中可用的JUnit 5扩展(最优先的选项
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>2.23.4</version>
    <scope>test</scope>
</dependency>

通过在测试类中添加@ExtendWith和用@Mock注解模拟字段来应用扩展,可以看出Mockito扩展的使用。

例如,如果我们需要测试SERVICE类并模拟数据库,我们需要使用以下代码。

public class Database {
    public boolean isAvailable() {
        // TODO implement the access to the database
        return false;
    }
    public int getUniqueId() {
        return 42;
    }
}
public class Service {
        private Database database;
        public Service(Database database) {
            this.database = database;
        }
        public boolean query(String query) {
            return database.isAvailable();
        }
        @Override
        public String toString() {
            return "Using database with id: " + String.valueOf(database.getUniqueId());
        }
}

然后,测试类将看起来像这样。

@ExtendWith(MockitoExtension.class)
public class ServiceTest {
        @Mock
        Database databaseMock;                                  
 
        @Test
        public void testQuery () {
            assertNotNull(databaseMock);
            when(databaseMock.isAvailable())
     .thenReturn(true);  
            Service t = new Service(databaseMock);             
            boolean check = t.query("* from t");                
            assertTrue(check);
        }
}

2.Selenium-Jupiter

通过结合Selenium(最受欢迎的网络浏览器测试框架)和JUnit 5的力量,selenium-jupiter允许使用本地和/或远程浏览器创建Selenium测试。有了它,你可以运行不同类型的测试来验证网络和移动应用程序的功能。此外,selenium-jupiter扩展还可用于Selenium自动化测试

用JUnit框架在云上进行Selenium自动化测试

在Maven项目中应使用以下依赖性。

</dependency>
	<!-- https://mvnrepository.com/artifact/io.github.bonigarcia/selenium-jupiter -->
	<dependency>
	    <groupId>io.github.bonigarcia</groupId>
	    <artifactId>selenium-jupiter</artifactId>
	    <version>3.4.0</version>
</dependency>

Selenium-Jupiter可以通过简单地使用SeleniumJupiter接口上的@ExtendWith注解来进行跨浏览器兼容性测试。下面是一个示例演示。

@ExtendWith(SeleniumJupiter.class)
public class CrossBrowserTest {
    @Test
    void testWithOneChrome(ChromeDriver chromeDriver) {
        // Use Chrome in this test
        chromeDriver.get("https://bonigarcia.github.io/selenium-jupiter/");
        Assertions.assertEquals(chromeDriver.getTitle(),
                "Selenium-Jupiter: JUnit 5 extension for Selenium");
    }
 
    @Test
    void testWithFirefox(FirefoxDriver firefoxDriver) {
        // Use Firefox in this test
        firefoxDriver.get("https://bonigarcia.github.io/selenium-jupiter/");
        Assertions.assertEquals(firefoxDriver.getTitle(),
                "Selenium-Jupiter: JUnit 5 extension for Selenium");
    }
 
    @Test
    void testWithChromeAndFirefox(ChromeDriver chromeDriver,
            FirefoxDriver firefoxDriver) {
        // Use Chrome and Firefox in this test
        chromeDriver.get("http://www.seleniumhq.org/");
        firefoxDriver.get("http://junit.org/junit5/");
        Assertions.assertEquals(chromeDriver.getTitle(), "SeleniumHQ Browser Automation");
        Assertions.assertEquals(firefoxDriver.getTitle(), "JUnit 5");
    }
}

阅读 -使用JUnit和Selenium进行浏览器兼容性的自动测试

如何使用Selenium-Jupiter进行Selenium自动化测试

Selenium-Jupiter支持通过DriverCapabilities和RemoteWebDriver的组合在Selenium网格上测试远程网络浏览器。你还可以通过使用LambdaTest在不同的浏览器和平台组合上运行测试,在Selenium中执行并行测试

@ExtendWith(SeleniumJupiter.class)
public class RemoteBrowserJupiterTest<WebDriver> {
    @DriverUrl
    String url = "http://localhost:4444/wd/hub";
 
    @BeforeAll
    static void setup() throws Exception {
        // Start hub
        GridLauncherV3.main(new String[] { "-role", "hub", "-port", "4444" });
 
        // Register Chrome in hub
        WebDriverManager.chromedriver().setup();
        GridLauncherV3.main(new String[] { "-role", "node", "-hub",
                "http://localhost:4444/grid/register", "-browser",
                "browserName=chrome", "-port", "5555" });
 
        // Register Firefox in hub
        WebDriverManager.firefoxdriver().setup();
        GridLauncherV3.main(new String[] { "-role", "node", "-hub",
                "http://localhost:4444/grid/register", "-browser",
                "browserName=firefox", "-port", "5556" });
    }
    @Test
    void testWithRemoteChrome(
            @DriverUrl("http://localhost:4444/wd/hub")
            @DriverCapabilities("browserName=chrome") RemoteWebDriver driver) {
        exercise(driver);
    }
 
    @Test
    void testWithRemoteFirefox(
                @DriverUrl("http://localhost:4444/wd/hub")
                @DriverCapabilities("browserName=firefox") RemoteWebDriver driver) {
        exercise(driver);
    }
 
    void exercise(WebDriver driver) {
        driver.get("https://bonigarcia.github.io/selenium-jupiter/");
        Assertions.assertEquals(driver.getTitle(),
                "Selenium-Jupiter: JUnit 5 extension for Selenium");
    }

如何使用Selenium-Jupiter进行移动设备测试

要创建一个ApiumDriver的实例来驱动移动设备,注释DriverCapabilities。Selenium-Jupiter将自动启动Appium服务器的一个实例。

@ExtendWith(SeleniumJupiter.class)
public class AppiumJupiterTest {
        @DriverUrl
        String url = "http://localhost:4723/wd/hub";
 
        @DriverCapabilities
        DesiredCapabilities capabilities = new DesiredCapabilities();
        {
            capabilities.setCapability("browserName", "chrome");
            capabilities.setCapability("deviceName", "Samsung Galaxy S6");
        }
 
        @Test
        void testWithAndroid(AppiumDriver<WebElement> driver) {
            driver.get("https://bonigarcia.github.io/selenium-jupiter/");
            Assertions.assertEquals(driver.getTitle(),
                    "JUnit 5 extension for Selenium");
        }
}

如何使用Selenium-Jupiter在云网格上执行Selenium自动化测试

Selenium-Jupiter让你在基于云的跨浏览器测试平台上运行Selenium自动化测试,如LambdaTest。云测试的主要好处是提高了浏览器的覆盖率,消除了与环境有关的进度延迟,提高了产品质量,并降低了总拥有成本(TCO)。请看我们的云测试教程,涵盖了将测试迁移到LambdaTest这样的云Selenium网格的无数好处。

在LambdaTest上创建一个账户后,请注意LambdaTest配置文件部分的用户名和访问权限。这些凭证是访问云网格所需的。然后,你可以使用LambdaTest能力生成器来生成所需的能力。

下面显示的是在LambdaTest网格上运行JUnit 5测试的一个例子。

@ExtendWith(SeleniumJupiter.class)
public class LambdaTestSeleniumJupiter {
    public RemoteWebDriver driver = null;
    String username = "mukendik";
    String accessKey = "mP7l3gCMXcLmwy7alMb6rAuqAOKcAAXMCklWlHLWbi8XhY0JWd";
 
    {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setCapability("platform", "Windows 7"); // MacOS Catalina Windows 10   
        capabilities.setCapability("browserName", "Chrome");
        capabilities.setCapability("version", "91.0"); // If this cap isn't specified, it will just get the any available one
        capabilities.setCapability("resolution","1024x768");
        capabilities.setCapability("build", "Selenium jupiter");
        capabilities.setCapability("name", "LambdaTest selenium jupiter");
        capabilities.setCapability("network", true); // To enable network logs
        capabilities.setCapability("visual", true); // To enable step by step screenshot
        capabilities.setCapability("video", true); // To enable video recording
        capabilities.setCapability("console", true); // To capture console logs
    
        try {       
            driver= new RemoteWebDriver(new URL("https://"+username+":"+accessKey+
                                  "@hub.lambdatest.com/wd/hub"), capabilities);            
        } catch (MalformedURLException e) {
            System.out.println("Invalid grid URL");
        }
    }
    @Test
    public void testWithLambdaTest() throws Exception {
                try {
                    driver.get("https://lambdatest.github.io/sample-todo-app/");
                    driver.findElement(By.name("li1")).click();
                    driver.findElement(By.name("li2")).click();
                    driver.findElement(By.id("sampletodotext")).clear();
                    driver.findElement(By.id("sampletodotext"))
                          .sendKeys("Hey, Let's add it to list");
                    driver.findElement(By.id("addbutton")).click();
                    driver.quit();                  
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
    }
}

这里是执行快照,表明测试执行成功了。

automation testing

3.3.Spring TestContext。Jupiter的SpringExtension

在Spring 5中引入的Spring TestContext是一个Spring框架,提供了与JUnit 5 Jupiter编程模型的完全集成。它可以在org.springframework.test.context.junit.jupiter.SpringExtension包中找到。

它可以通过简单地在JUnit Jupiter测试类中加入以下任何一个注解来使用。

  1. @ExtendWith(SpringExtension.class)
  2. @SpringJunitConfig(TestConfig.class)
  3. @SpringJUnitWebConfig(TestConfig.class)

下面显示的是一个JUnit 5扩展实例,演示了Spring TestContext的用法。

//Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
//Instructs Spring to load an ApplicationContext from AppConfig.class
@ContextConfiguration(classes = AppConfig.class)
public class SpringExtensionTest {
    
    @Autowired
    private MyService myService;
 
    @BeforeAll
    static void initAll() {
        System.out.println("---Inside initAll---");
    }
 
    @BeforeEach
    void init(TestInfo testInfo) {
        System.out.println("Start..." + testInfo.getDisplayName());
    }
 
    @Test
    public void messageTest() {
        String msg = myService.getMessage();
        assertEquals("Hello World!", msg);
    }
 
    @Test
    public void multiplyNumTest() {
        int val = myService.multiplyNum(5, 10);
        assertEquals(50, val);
    }
 
    @Test
    public void idAvailabilityTest() {
        boolean val = myService.isIdAvailable(100);
        Assertions.assertTrue(val);
    }
 
    @AfterEach
    void tearDown(TestInfo testInfo) {
        System.out.println("Finished..." + testInfo.getDisplayName());
    }
 
    @AfterAll
    static void tearDownAll() {
        System.out.println("---Inside tearDownAll---");
    }
 
}

@Configuration
@ComponentScan("com.concretepage")
public class AppConfig {
 
}

@Service
public class MyService {
 
    public String getMessage() {
        return "Hello World!";
    }
    
     public int multiplyNum(int num1, int num2) {
            return num1 * num2;
        }
     
    public boolean isIdAvailable(long id) {
        if (id == 100) {
            return true;
        }
        return false;
    }    
}

public class SpringProfileDemo {
 
    public static void main(String[] args) {
        
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.scan("com.concretepage");
        ctx.refresh();
        MyService myService = ctx.getBean(MyService.class);
        System.out.println(myService.getMessage()); 
 
    }
}

cross browser testing

结论和建议

Jupiter中内置的JUnit 5扩展模型已经解决了JUnit 4扩展点的固有问题。该模型实现了多个内置的扩展点,并允许其定制和分组使用。这允许扩展开发者在现有的一个中实现接口,以纳入JUnit 5的额外功能。

JUnit 5的扩展允许增强和扩展JUnit的能力。然而,一些框架也有完全集成和适应的JUnit扩展点,允许其重用,使Jupiter扩展模型更加强大,并根据环境和情况简化测试。因此,强烈建议使用这些扩展点,不管是集成的还是定制的,以使测试更加可靠。

资料来源

这篇文章没有详尽地介绍所有与JUnit 5集成的扩展点,甚至没有介绍所有第三方库的扩展。因此,如果你对这里没有显示的扩展点或第三方扩展框架感兴趣,你可以让我们知道,以便根据读者的兴趣完成本指南。

我们也可以把那些你在本指南中看来不清楚的地方再详细地发展一下。我们也希望得到您在各自项目中使用JUnit Jupiter扩展点的反馈。上述例子的源代码可以在GitHub上找到。

常见的问题

什么是JUnit 5扩展?

JUnit 5扩展点引入了一种全新的扩展和配置单元测试行为的方法。扩展点指的是在运行自动测试的过程中满足某些条件或事件时被调用的方法。

什么是JUnit测试文件的扩展?

JUnit测试文件写在以.java文件为扩展名的文件中。

如何将JUnit 5添加到构建路径中?

按照下面提到的步骤在 Eclipse IDE 中添加 JUnit 5 库文件。

  1. 右键单击项目文件夹,选择配置构建路径...
  2. 选择JRE库下的 "库 "选项卡,如果没有添加JUnit相关的jar文件,请添加。