JUnit 5和Mockito进行单元测试

122 阅读9分钟

  文章转载链接www.51testing.com/html/04/n-7…

  1. JUnit 5 基础

  JUnit 5是最新的JUnit版本,它引入了许多新特性,包括更灵活的测试实例生命周期、参数化测试、更丰富的断言和假设等。

  1.1 基本注解

  @Test:标记一个方法为测试方法。

  @BeforeEach:在每个测试方法之前执行。

  @AfterEach:在每个测试方法之后执行。

  @BeforeAll:在所有测试方法之前执行一次(必须是静态方法)。

  @AfterAll:在所有测试方法之后执行一次(必须是静态方法)。

  @DisplayName:定义测试类或测试方法的自定义名称。

  @Nested:允许将测试类分组到更小的测试类中。

  @ParameterizedTest:进行参数化测试。

  1.2 断言(Assertions类)

  Assertions 类是JUnit 5中用于断言的核心类,提供了一系列静态方法来验证测试条件是否满足预期。

  assertEquals(expected, actual):验证两个对象是否相等。如果不等,测试失败。

  assertTrue(boolean condition):验证条件是否为真。如果为假,测试失败。

  assertFalse(boolean condition):验证条件是否为假。如果为真,测试失败。

  assertNull(Object object):验证对象是否为null。如果不为null,测试失败。

  assertNotNull(Object object):验证对象是否不为null。如果为null,测试失败。

  assertThrows(Class expectedType, Executable executable):验证执行executable是否抛出了expectedType类型的异常。如果没有抛出或抛出其他类型的异常,测试失败。

  assertAll(Executable... executables):同时执行多个断言,如果所有断言都成功,则测试通过;如果任何一个断言失败,所有失败的断言都会被报告。

  assertSame(expected, actual):验证两个对象是否为同一个对象(使用==比较)。如果不是,测试失败。

  assertNotSame(unexpected, actual):验证两个对象是否不是同一个对象(使用==比较)。如果是,测试失败。

  assertTimeout(Duration timeout, Executable executable):验证执行executable是否在给定的时间内完成。如果执行超时,测试失败。

  1.3  Assumptions 类

  Assumptions 类提供了基于某些条件判断是否执行测试的能力。如果假设失败(即条件不满足),当前测试会被跳过,而不是失败。以下是一些常用的Assumptions方法:

  assumeTrue(boolean assumption):如果假设为真,则继续执行测试;如果假设为假,测试被跳过。

  assumeFalse(boolean assumption):如果假设为假,则继续执行测试;如果假设为真,测试被跳过。

  assumingThat(boolean assumption, Executable executable):如果假设为真,则执行给定的executable(可以是一个测试方法);无论假设结果如何,测试都会继续执行,但executable只在假设为真时执行。

  2.Mockito 基础

  Mockito是一个流行的Java mocking框架,用于在隔离环境中测试代码,通过模拟依赖来确保测试的独立性。

  基本注解:

  @Mock:创建一个模拟对象。

  @InjectMocks:创建一个实例,其字段或构造器依赖将被@Mock注解的模拟对象自动注入。

  @Spy:可以创建一个真实的对象,并在需要时对它的某些方法进行模拟。

  @Captor:用于捕获方法调用的参数。

  常用方法:

  mock(Class classToMock):创建一个类的模拟对象。这是创建模拟对象的基础。

  when(T methodCall):当你想模拟一个方法调用的返回值时使用。与thenReturn一起使用,可以指定一个方法调用应该返回什么值。

  thenReturn(T value):与when方法一起使用,用于指定方法调用的返回值。

  doReturn(Object toBeReturned):一个替代thenReturn的方法,用在当你需要模拟void方法或在spy对象上进行模拟时。

  verify(T mock):用于验证某个模拟对象的某个方法是否被调用,以及调用的次数。

  any():在设定模拟行为(如when)或验证(如verify)时,用于表示任何类型和值的参数。

  eq(T value):用于指定方法调用时期望的具体参数值。

  doNothing():用于模拟void方法时,指定该方法不执行任何操作。

  doThrow(Throwable... toBeThrown):用于模拟方法调用时抛出异常。

  spy(T object):创建一个真实对象的“间谍”或“spy”。这允许你在真实对象上“监视”方法调用,同时还能够覆盖某些方法的行为。

  ArgumentCaptor:用于捕获方法调用时传递的参数,以便后续进行断言。

  times(int wantedNumberOfInvocations):与verify方法一起使用,用于指定某个方法被调用的具体次数。

  never():与verify一起使用,用于验证某个方法从未被调用过。

  示例

  假设我们有一个PaymentService类,它依赖于PaymentProcessor接口:

   public class PaymentService {

      private PaymentProcessor processor;

   

      public PaymentService(PaymentProcessor processor) {

          this.processor = processor;

      }

   

      public boolean process(double amount) {

          return processor.processPayment(amount);

      }

  }   

  下面是如何使用JUnit 5和Mockito来测试PaymentService类:

      import org.junit.jupiter.api.Test;

  import org.junit.jupiter.api.extension.ExtendWith;

  import org.mockito.Mock;

  import org.mockito.junit.jupiter.MockitoExtension;

  import static org.mockito.Mockito.*;

  import static org.junit.jupiter.api.Assertions.*;

   

  @ExtendWith(MockitoExtension.class)

  public class PaymentServiceTest {

   

      @Mock

      PaymentProcessor processor;

   

      @Test

      public void testProcessPayment() {

          // 设置

          PaymentService service = new PaymentService(processor);

          double amount = 100.0;

          when(processor.processPayment(amount)).thenReturn(true);

   

          // 执行

          boolean result = service.process(amount);

   

          // 验证

          assertTrue(result);

          verify(processor).processPayment(amount);

      }

  }

  在这个例子中,我们使用@Mock来创建PaymentProcessor的模拟对象,并使用when(...).thenReturn(...)来定义当调用processPayment方法时应返回的值。然后,我们执行process方法,并使用assertTrue来验证结果是否符合预期。最后,我们使用verify来确认processPayment方法是否被正确调用。

  3.JUnit 5 进阶用法

  参数化测试(Parameterized Tests)

  参数化测试允许你使用不同的参数多次运行同一个测试。这对于需要验证多种输入条件的方法特别有用。

      @ParameterizedTest

  @ValueSource(strings = {"Hello", "JUnit"})

  void withValueSource(String word) {

      assertNotNull(word);

  }

  动态测试(Dynamic Tests)

  JUnit 5允许你动态生成测试,这些测试可以在运行时根据代码逻辑来决定。

      @TestFactory

  Collection<DynamicTest> dynamicTests() {

      return Arrays.asList(

          dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))),

          dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))

      );

  }

 嵌套测试(Nested Tests)

  使用@Nested注解,你可以将相关的测试组织在一起作为一个组在外层测试类中运行。

      @Nested

  class WhenNew {

      @Test

      void isEmpty() {

          assertEquals(0, new ArrayList<>().size());

      }

      @Nested

      class AfterAddingAnElement {

          @Test

          void isNotEmpty() {

              List<Object> list = new ArrayList<>();

              list.add(new Object());

              assertEquals(1, list.size());

          }

      }

  }

  超时测试

  JUnit 5允许你为测试设置超时时间,确保测试在给定时间内完成。如果超出指定时间,测试将失败。

      @Test

  @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)

  void timeoutTest() {

      // 模拟一个耗时的操作

      // 如果操作超过500毫秒,则测试失败

  }

  重复测试

  如果你想对一个测试方法进行多次执行以确保其稳定性或寻找潜在的偶发问题,可以使用@RepeatedTest注解。

      @RepeatedTest(5)

  void repeatTest() {

      // 这个测试会运行5次

  }

  条件执行

  JUnit 5提供了多种条件执行测试的方法,这些方法可以基于不同的条件来决定是否执行某个测试,例如操作系统类型、环境变量或Java版本。

      @Test

  @EnabledOnOs(OS.WINDOWS)

  void onlyOnWindows() {

      // 仅在[**Windows**]()操作系统上运行

  }

  @Test

  @EnabledIfSystemProperty(named = "user.name", matches = "yourUserName")

  void onlyForSpecificUser() {

      // 仅当系统用户名匹配时运行

  }

  4.Mockito 进阶用法

  使用@Spy进行部分模拟

  有时你可能需要模拟类的某些方法,而保持其他方法的实际行为。@Spy注解允许你这样做。

      @Spy

  List<String> spyList = new ArrayList<>();

  @Test

  void testSpy() {

      spyList.add("one");

      spyList.add("two");

      verify(spyList).add("one");

      verify(spyList).add("two");

      assertEquals(2, spyList.size()); // 实际调用方法

      // 修改方法行为

      doReturn(100).when(spyList).size();

      assertEquals(100, spyList.size()); // 方法行为被改变

  }

  参数捕获(Argument Captors)

  有时在验证方法调用时,你可能对方法调用的具体参数值感兴趣。@Captor注解和ArgumentCaptor类允许你捕获和检查这些值。

      @Mock

  List<String> mockList;

  @Captor

  ArgumentCaptor<String> argCaptor;

  @Test

  void argumentCaptorTest() {

      mockList.add("one");

      verify(mockList).add(argCaptor.capture());

      assertEquals("one", argCaptor.getValue());

  }

  连续调用的不同返回值

  有时候,你可能需要一个方法在连续调用时返回不同的值。Mockito允许你通过thenReturn()方法链来实现这一点。

      when(mockList.size()).thenReturn(0).thenReturn(1);

  assertEquals(0, mockList.size());

  assertEquals(1, mockList.size());

  验证调用次数

  验证一个方法被调用了特定次数。

      mockList.add("once");

  mockList.add("twice");

  mockList.add("twice");

  verify(mockList).add("once");

  verify(mockList, times(2)).add("twice");

  verify(mockList, never()).add("never happened");

 模拟静态方法(需要Mockito 3.4.0及以上版本)

  从Mockito 3.4.0开始,你可以使用mockStatic来模拟静态方法。这是通过try-with-resources语句来实现的,以确保静态mock在使用后被正确关闭。

      try (MockedStatic<UtilityClass> mockedStatic = mockStatic(UtilityClass.class)) {

      mockedStatic.when(UtilityClass::someStaticMethod).thenReturn("mocked response");

      assertEquals("mocked response", UtilityClass.someStaticMethod());

      // 静态方法被模拟期间的行为

  }

  // 在这个块之外,静态方法恢复原有行为

  模拟final方法和类

  Mockito 2.x开始支持模拟final方法和类。为了启用这个功能,你需要在src/test/resources/mockito-extensions目录下创建一个名为org.mockito.plugins.MockMaker的文件,并在文件中添加一行内容:

  mock-maker-inline

  这样配置后,Mockito就可以模拟final类和方法了。

  使用BDDMockito进行行为驱动开发

  BDDMockito提供了一种基于行为驱动开发(BDD)的语法来编写Mockito测试,使得测试更加可读。

    
  @Test

  void bddStyleTest() {

      // 给定

      BDDMockito.given(mockList.size()).willReturn(2);

      // 当

      int size = mockList.size();

      // 那么

      BDDMockito.then(mockList).should().size();

      assertEquals(2, size);

  }

  5. @Mock 、@InjectMocks的原理

  @Mock

  · 原理: @Mock注解告诉Mockito框架为标注的字段生成一个模拟对象。这个模拟对象是动态生成的代理对象,它拦截对任何非final方法的调用,并允许测试者通过Mockito的API来配置这些调用的行为(例如返回特定的值或抛出异常)。

  · 如何工作: 当测试初始化时(例如,通过使用MockitoAnnotations.initMocks(this)方法或JUnit 5的@ExtendWith(MockitoExtension.class)),Mockito会扫描测试类中所有使用@Mock注解的字段,并为它们创建模拟对象。这些模拟对象默认不执行任何实际的代码逻辑,它们的行为完全由测试者通过Mockito的API来控制。

  @InjectMocks

  · 原理: @InjectMocks注解用于自动将@Mock(或@Spy)注解创建的模拟对象注入到被注解的字段中。Mockito会尝试通过构造器注入、属性注入或setter方法注入的方式,将模拟对象注入到@InjectMocks标注的实例中。

 · 如何工作:

  构造器注入:Mockito首先尝试使用包含最多参数的构造器来创建实例。如果构造器的参数能够与已声明的模拟对象匹配,这些模拟对象将被用作构造器参数。

  属性注入:如果构造器注入不适用或不成功,Mockito会尝试直接设置实例中与模拟对象类型相匹配的属性。

  Setter注入:最后,如果属性注入不成功,Mockito会尝试通过调用匹配的setter方法来注入模拟对象。