使用Mockito进行模拟和测试桩

如果您觉得本博客的内容对您有所帮助或启发,请关注我的博客,以便第一时间获取最新技术文章和教程。同时,也欢迎您在评论区留言,分享想法和建议。谢谢支持!​


Mockito是一个流行的Java模拟框架,用于编写单元测试代码时模拟(mock)和测试桩(stub)对象的行为。可轻松模拟Java类和接口的行为,帮助测试人员和开发人员更好地设计和执行单元测试。

使用Mockito,开发人员可以模拟一个对象,使其表现出某些预期的行为,而无需使用真实对象。这种技术通常用于在不使用复杂的集成测试环境的情况下测试代码。Mockito可以协助进行单元测试、集成测试和行为驱动开发(BDD)。

一、Mockito基础知识

1、Mockito的优点

  • 使用简单:Mockito的API简单明了,易于学习和使用。
  • 支持多种场景:Mockito支持各种测试场景,如单元测试、集成测试和BDD等。
  • 良好的文档:Mockito拥有全面的文档和用户群体,可以提供许多使用方案和实例。

2、Mockito的局限性

  • 不支持静态方法和final方法的模拟。
  • 可能会过度使用,导致测试代码的维护难度增加。

3、Mockito的常见概念

  • Mock:指一个对象的虚拟实现,具有与真实对象相同的方法和属性,但不会真正执行其中的方法。
  • Stub:指为某个方法调用提供预定义返回值的代码,通常用于控制测试中的代码路径。
  • Verify:指验证Mock对象是否按照预期进行了交互。Verify可用于验证Mock对象的方法是否被调用了特定的次数,并且传入了预期的参数。

4、Mockito的常见用法

创建Mock对象

List mockList = mock(List.class);

Stub方法调用

when(mockList.get(0)).thenReturn("first");

验证方法调用

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

模拟方法抛出异常

when(mockList.get(anyInt())).thenThrow(new RuntimeException());

模拟连续调用

when(mockList.get(anyInt())).thenReturn("one", "two", "three");

Mockito提供了许多其他功能,如ArgumentMatchers用于匹配方法调用的参数、Annotations用于对Mock对象进行注释、Spy用于监视真实对象等等。通过学习和掌握Mockito的使用,可以更加高效地进行单元测试和集成测试。

二、使用Mockito进行模拟

1、使用Mockito进行模拟的步骤和示例

Mockito可以通过模拟对象来测试代码,步骤如下:

  1. 导入Mockito库。在pom.xml文件中添加以下依赖:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.12.4</version>
    <scope>test</scope>
</dependency>
  1. 创建要测试的类和方法

public class UserService {
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public User getUserById(int id) {
        return userDao.getUserById(id);
    }
}

public interface UserDao {
    User getUserById(int id);
}
  1. 创建一个模拟对象

UserDao userDao = mock(UserDao.class);
  1. 设置模拟对象的行为

when(userDao.getUserById(1)).thenReturn(new User(1, "John"));
  1. 运行测试代码

@Test
public void testGetUserById() {
    UserDao userDao = mock(UserDao.class);
    when(userDao.getUserById(1)).thenReturn(new User(1, "John"));

    UserService userService = new UserService(userDao);
    User user = userService.getUserById(1);

    assertEquals(user.getId(), 1);
    assertEquals(user.getName(), "John");
}

2、使用when()

Mockito的when()方法可以用于设置模拟对象的行为,例如:

when(mockObject.someMethod()).thenReturn(someValue);

示例代码:

@Test
public void testGetUserById() {
    UserDao userDao = mock(UserDao.class);
    when(userDao.getUserById(1)).thenReturn(new User(1, "John"));

    UserService userService = new UserService(userDao);
    User user = userService.getUserById(1);

    assertEquals(user.getId(), 1);
    assertEquals(user.getName(), "John");
}

3、使用doReturn()

doReturn()方法与when()方法类似,可以用于设置模拟对象的行为,例如:

doReturn(someValue).when(mockObject).someMethod();

示例代码:

@Test
public void testGetUserById() {
    UserDao userDao = mock(UserDao.class);
    doReturn(new User(1, "John")).when(userDao).getUserById(1);

    UserService userService = new UserService(userDao);
    User user = userService.getUserById(1);

    assertEquals(user.getId(), 1);
    assertEquals(user.getName(), "John");
}

4、使用mock()方法创建模拟对象

mock()方法可以用于创建模拟对象,例如:

SomeClass mockObject = mock(SomeClass.class);

示例代码:

@Test
public void testGetUserById() {
    UserDao userDao = mock(UserDao.class);
    when(userDao.getUserById(1)).thenReturn(new User(1, "John"));

    UserService userService = new UserService(userDao);
    User user = userService.getUserById(1);

    assertEquals(user.getId(), 1);
    assertEquals(user.getName(), "John");
}

4、使用@Mock注解创建模拟对象

除了使用 ​​mock()​​​ 方法创建模拟对象外,还可以使用 ​​@Mock​​ 注解来创建模拟对象。

首先需要在测试类中使用 ​​@RunWith(MockitoJUnitRunner.class)​​ 注解,以便在运行测试时自动初始化模拟对象。

接着在测试类中使用 ​​@Mock​​ 注解创建模拟对象,如下所示:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    
    @Mock
    private UserDao userDao;
    
    @Test
    public void testGetUserById() {
        UserService userService = new UserService(userDao);
        
        // ...
    }
}

使用 ​​@Mock​​ 注解创建模拟对象时,需要注意以下几点:

  • 被 ​​@Mock​​​ 注解修饰的变量不能为 ​​null​​。
  • 被 ​​@Mock​​​ 注解修饰的变量默认为 ​​@Mock(answer = RETURNS_DEFAULTS)​​。
  • 可以通过 ​​@Mock(answer = Answers.RETURNS_SMART_NULLS)​​​ 显式指定返回智能 ​​null​​。

使用 ​​@Mock​​ 注解创建模拟对象时,Mockito 会自动创建并初始化模拟对象,并将其注入测试类中。

注意,使用 ​​@Mock​​​ 注解创建的模拟对象需要在测试类中使用,否则会抛出 ​​UnnecessaryStubbingException​​ 异常。

5、使用@Spy注解进行模拟对象的部分模拟

除了使用@Mock注解创建一个完整的模拟对象之外,Mockito还提供了@Spy注解来创建部分模拟对象,这样可以在保留真实对象部分行为的同时,对其它行为进行模拟。

下面是@Spy注解的使用示例:

public class ExampleServiceTest {
    
    @Spy
    private ExampleServiceImpl exampleServiceSpy;
    
    @Test
    public void testSomeMethod() {
        // 对exampleServiceSpy进行部分模拟,保留真实对象的部分行为
        Mockito.doCallRealMethod().when(exampleServiceSpy).someMethod();
        
        // 对someOtherMethod进行模拟
        Mockito.when(exampleServiceSpy.someOtherMethod()).thenReturn("mocked result");
        
        // 执行测试代码,调用exampleServiceSpy.someMethod()方法
        exampleServiceSpy.someMethod();
        
        // 验证someMethod()方法中是否调用了someOtherMethod()方法
        Mockito.verify(exampleServiceSpy).someOtherMethod();
    }
}

在这个例子中,我们使用了@Spy注解来创建一个ExampleServiceImpl对象的部分模拟。然后,我们使用了​​Mockito.doCallRealMethod().when(exampleServiceSpy).someMethod();​​来保留exampleServiceSpy对象中someMethod()方法的真实行为,而对someOtherMethod()方法进行了模拟。最后,我们调用了exampleServiceSpy.someMethod()方法,并验证了someOtherMethod()方法是否被调用。

三、使用Mockito进行测试桩

1、测试桩的作用和场景

使用Mockito进行测试桩可以在单元测试中模拟方法的返回值或抛出异常,以便测试被测代码在各种情况下的行为。常见的使用场景包括:

  1. 测试被测代码在异常情况下的行为。通过测试桩可以模拟方法抛出异常的场景,测试被测代码在异常情况下是否能够正确地处理异常。
  2. 测试被测代码在特定情况下的行为。例如,测试一个方法在输入为null时的行为,可以使用测试桩模拟方法的输入为null的场景。
  3. 测试被测代码与外部依赖的交互。通过模拟外部依赖的返回值或抛出异常,可以测试被测代码在与外部依赖交互时的行为。
  4. 测试被测代码的边界条件。通过模拟外部依赖或方法的返回值,可以测试被测代码在各种边界情况下的行为,例如输入为最大值或最小值的情况。

总之,测试桩可以帮助开发人员创建各种测试场景,以确保被测代码的行为正确。

2、使用Mockito进行测试桩的步骤和示例

使用Mockito进行测试桩可以模拟需要的返回值或异常,以便在测试中测试需要的场景。以下是使用Mockito进行测试桩的步骤和示例:

  1. 创建需要进行测试桩的对象或接口:

public interface UserService {
    User getUserById(int userId);
}
  1. 使用Mockito进行测试桩,例如模拟getUserById方法返回指定的User对象:

@Test
public void testGetUserById() {
    UserService userService = Mockito.mock(UserService.class);
    User expectedUser = new User("Alice", 20);
    Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(expectedUser);
    
    User actualUser = userService.getUserById(1);
    
    Assert.assertEquals(expectedUser, actualUser);
}

在这个示例中,我们使用Mockito.mock()方法创建了一个UserService对象的模拟对象userService,并使用Mockito.when()方法对getUserById方法进行测试桩,指定了当传入任何整数时返回一个指定的User对象。然后我们使用模拟对象userService调用getUserById方法,并断言返回的User对象是否是我们期望的值。

  1. 使用测试桩模拟抛出异常:

@Test(expected = UserNotFoundException.class)
public void testGetUserByIdWhenUserNotFound() {
    UserService userService = Mockito.mock(UserService.class);
    Mockito.when(userService.getUserById(Mockito.anyInt())).thenThrow(UserNotFoundException.class);
    
    userService.getUserById(1);
}

在这个示例中,我们使用Mockito.when()方法对getUserById方法进行测试桩,指定了当传入任何整数时抛出一个UserNotFoundException异常。然后我们使用模拟对象userService调用getUserById方法,并期望捕获UserNotFoundException异常。

3、使用thenReturn()方法设置桩值

除了使用doReturn()方法进行桩方法之外,我们还可以使用thenReturn()方法来设置桩值。当桩方法返回一个值时,我们可以使用thenReturn()方法来指定这个值。例如:

@Test
public void testGetPerson() {
    // 创建模拟对象
    PersonDao personDao = mock(PersonDao.class);

    // 设置桩方法并返回模拟数据
    when(personDao.getPerson(1)).thenReturn(new Person("Alice", 20));

    // 执行被测试方法
    PersonService personService = new PersonService(personDao);
    Person person = personService.getPerson(1);

    // 验证方法的返回值是否正确
    assertEquals("Alice", person.getName());
    assertEquals(20, person.getAge());
}

在这个例子中,我们通过when()方法设置桩方法,并使用thenReturn()方法指定了当getPerson()方法传入参数1时应该返回的模拟数据。然后,我们执行被测试的方法,并使用assertEquals()方法验证方法的返回值是否正确。如果方法返回了我们预期的模拟数据,那么测试就通过了。

4、使用thenThrow()方法抛出异常

Mockito 的 thenThrow() 方法可以用来设置测试桩方法在执行时抛出指定异常。这对于测试某些异常情况下的代码行为非常有用。

使用 thenThrow() 方法非常简单,只需要在桩方法后调用 thenThrow() 方法,并传入要抛出的异常类型即可。以下是一个示例:

@Test
public void testDoSomething() throws Exception {
    SomeObject mockObject = mock(SomeObject.class);
    when(mockObject.doSomething()).thenThrow(new RuntimeException("test exception"));
    
    // 确保调用 doSomething() 时会抛出 RuntimeException
    assertThrows(RuntimeException.class, () -> {
        mockObject.doSomething();
    });
}

在上面的示例中,我们使用 ​​when()​​​ 方法对 ​​doSomething()​​​ 方法进行桩,然后调用 ​​thenThrow()​​​ 方法并传入一个 ​​RuntimeException​​​ 对象。然后我们调用 ​​doSomething()​​​ 方法,这时候会抛出一个运行时异常。最后,我们使用 JUnit 的 ​​assertThrows()​​ 方法来验证方法确实抛出了运行时异常。

总的来说,使用 ​​thenThrow()​​ 方法可以帮助我们测试代码在异常情况下的行为。

5、使用doAnswer()方法自定义桩方法

在某些情况下,可能需要自定义桩方法来满足测试的需要。这时可以使用Mockito的doAnswer()方法来实现。

doAnswer()方法如下:

public <T> OngoingStubbing<T> doAnswer(Answer<?> answer)

doAnswer()方法的参数是一个Answer对象,该对象表示自定义的桩方法的行为。Answer接口中有一个方法​​answer()​​,该方法返回一个泛型对象,表示模拟方法的返回值。

下面是一个使用doAnswer()方法自定义桩方法的示例:

List<String> list = mock(List.class);
doAnswer(invocation -> {
    Object[] args = invocation.getArguments();
    String result = (String) args[0] + "Mockito";
    return result;
}).when(list).get(anyInt());

这个例子中,我们自定义了List的get方法,将其返回值修改为输入参数的字符串后面加上"Mockito"。可以看到,在doAnswer()方法中,我们实现了Answer接口的​​answer()​​方法,并使用Invocation对象来获取传入的参数和返回值。最后使用when()方法来应用桩方法。

6、使用@Captor注解进行参数捕获

Mockito提供了@Captor注解来捕获模拟对象方法调用中传入的参数。这个注解可以在测试用例中声明一个参数,并将其注解为@Captor,Mockito会自动将模拟对象方法调用中的参数注入到这个参数中,以便我们进行断言或其他操作。

使用@Captor注解进行参数捕获的步骤和示例如下:

  1. 在测试用例类中创建@Captor注解,并初始化一个参数,例如:

@Captor
private ArgumentCaptor<String> captor;
  1. 在测试用例中使用模拟对象调用方法,并将参数传递给模拟对象,例如:

mockObject.doSomething("test");
  1. 使用Mockito.verify()方法验证模拟对象方法的调用,并使用@Captor注解捕获方法调用时传递的参数,例如:

verify(mockObject).doSomething(captor.capture());
  1. 对捕获的参数进行断言或其他操作,例如:

assertEquals("test", captor.getValue());

这样,就可以使用@Captor注解进行参数捕获,方便我们在测试用例中对方法参数进行断言和其他操作。

四、Mockito进阶用法

1、使用Mockito进行异步测试

在异步编程中,我们经常需要对异步方法进行测试,确保它们能够按照预期工作。Mockito提供了一些方法来处理异步测试场景,包括异步回调和等待异步结果。

下面是使用Mockito进行异步测试的一些常见场景和示例。

模拟异步回调

在异步回调中,当一个异步操作完成时,它将调用一个回调函数来通知调用方。Mockito提供了​​Answer​​接口,可以使用它来模拟异步回调函数。

示例:

@Test
public void testAsyncCallback() {
    MyAsyncService service = mock(MyAsyncService.class);
    when(service.doSomethingAsync(anyString(), any(Consumer.class))).thenAnswer(new Answer<Void>() {
        @Override
        public Void answer(InvocationOnMock invocation) throws Throwable {
            Object[] args = invocation.getArguments();
            String arg1 = (String) args[0];
            Consumer<String> callback = (Consumer<String>) args[1];
            callback.accept(arg1 + " is done");
            return null;
        }
    });

    MyAsyncClient client = new MyAsyncClient(service);
    String result = client.doSomething("test");

    assertEquals("test is done", result);
}

在这个示例中,我们使用​​Answer​​​接口来模拟异步回调函数。当​​service.doSomethingAsync​​​方法被调用时,我们从参数中获取回调函数并执行它,然后返回​​null​​。在测试中,我们验证异步客户端返回的结果是否正确。

等待异步结果

在异步编程中,我们经常需要等待异步操作完成后获取结果。为了测试异步方法,我们需要等待异步操作完成后再断言结果。Mockito提供了一些方法来处理这种场景。

示例:

@Test
public void testAsyncResult() throws Exception {
    MyAsyncService service = mock(MyAsyncService.class);
    CompletableFuture<String> future = new CompletableFuture<>();
    when(service.doSomethingAsync(anyString())).thenReturn(future);

    MyAsyncClient client = new MyAsyncClient(service);
    CompletableFuture<String> result = client.doSomethingAsync("test");

    assertFalse(result.isDone()); // 验证异步方法还未完成

    future.complete("test is done");

    assertTrue(result.isDone()); // 验证异步方法已完成
    assertEquals("test is done", result.get()); // 验证异步方法的结果是否正确
}

在这个示例中,我们使用​​CompletableFuture​​​来模拟异步方法的结果。当​​service.doSomethingAsync​​​方法被调用时,我们返回一个​​CompletableFuture​​​对象。在测试中,我们验证异步方法是否已经启动,然后手动完成​​CompletableFuture​​对象并验证结果是否正确。

需要注意的是,在使用​​CompletableFuture​​​对象进行异步测试时,我们需要等待异步操作完成后再获取结果。我们可以使用​​isDone()​​​方法来判断异步操作是否完成,使用​​get()​​方法来获取异步操作的结果。

2、使用Mockito进行参数匹配

在使用 Mockito 进行单元测试时,我们通常需要对被测方法传入不同的参数进行测试。但有时候我们希望只测试特定的参数组合,这时候就需要使用参数匹配。

Mockito 提供了一系列的参数匹配器,可以根据参数类型和值来匹配参数。常用的参数匹配器有:

  • any():匹配任何对象,例如 any(String.class) 匹配任何 String 类型的参数。
  • eq():匹配指定的对象,例如 eq("abc") 匹配参数值为 "abc" 的参数。
  • isA():匹配指定类型的参数,例如 isA(String.class) 匹配参数类型为 String 的参数。
  • anyXxx():匹配指定类型的基本数据类型,例如 anyInt() 匹配任何 int 类型的参数。

下面是使用 Mockito 进行参数匹配的示例代码:

// 定义被测类
public class UserService {
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public User getUserByName(String name) {
        return userDao.getUserByName(name);
    }
}

// 定义 UserDao 接口
public interface UserDao {
    User getUserByName(String name);
}

// 定义测试类
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    @Mock
    private UserDao userDao;

    @InjectMocks
    private UserService userService;

    @Test
    public void testGetUserByName() {
        // 设置桩方法
        when(userDao.getUserByName(any(String.class)))
                .thenReturn(new User("Tom"));

        // 调用被测方法
        User user = userService.getUserByName("Tom");

        // 验证返回值
        assertEquals("Tom", user.getName());
    }
}

在上面的示例代码中,我们使用了 any() 方法来匹配 getUserByName() 方法的参数,这样就可以匹配任何字符串类型的参数,不需要具体指定参数值。这样可以使测试代码更加灵活和通用。

除了上面介绍的参数匹配器外,Mockito 还提供了很多其他的参数匹配器,具体可以参考 Mockito 的官方文档。

3、使用Mockito进行void方法的桩方法和验证

Mockito可以用于桩方法和验证void方法,下面将介绍如何使用Mockito来进行void方法的桩方法和验证。

void方法的桩方法

Mockito中有两种方法可以用于void方法的桩方法,分别是doNothing()和doThrow()。

  • doNothing():表示当void方法被调用时,不做任何事情。
  • doThrow():表示当void方法被调用时,抛出一个指定的异常。

下面是示例代码:

// 创建一个mock对象
List<String> mockList = mock(List.class);

// 对void方法进行桩方法,表示当调用add方法时,不做任何事情
doNothing().when(mockList).add(anyString());

// 对void方法进行桩方法,表示当调用clear方法时,抛出一个RuntimeException异常
doThrow(new RuntimeException()).when(mockList).clear();

void方法的验证

Mockito中使用verify()方法来验证void方法是否被调用,和之前提到的verify()方法类似,只是不需要设置返回值。

下面是示例代码:

// 创建一个mock对象
List<String> mockList = mock(List.class);

// 调用void方法
mockList.clear();

// 验证clear方法是否被调用过一次
verify(mockList).clear();

使用Mockito进行void方法的桩方法和验证和普通方法类似,只需要使用doNothing()、doThrow()方法进行桩方法,使用verify()方法进行验证即可。

4、使用Mockito进行mock静态方法和final方法

Mockito 无法直接 Mock 静态方法和 final 方法,因为它们不能被子类化和重载,但是 Mockito 可以与 PowerMock 等其他 Mock 框架结合使用来 Mock 静态方法和 final 方法。

PowerMock 是一个 Java 开源框架,它结合了 EasyMock 和 Mockito 的功能,并添加了对静态方法、final 方法、私有方法、构造函数和静态初始化块的支持。

下面是使用 PowerMock 和 Mockito 来 Mock 静态方法和 final 方法的步骤和示例:

1 在 Maven POM 文件中添加 PowerMock 和 Mockito 的依赖项:

<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-core</artifactId>
  <version>2.0.9</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-module-junit4</artifactId>
  <version>2.0.9</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>3.2.4</version>
  <scope>test</scope>
</dependency>

2 使用 @RunWith(PowerMockRunner.class) 和 @PrepareForTest 注解来准备需要 Mock 的类:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ClassToMock.class})
public class MyClassTest {
  ...
}

3 使用 PowerMockito.mockStatic(ClassToMock.class) 方法来 Mock 静态方法:

PowerMockito.mockStatic(ClassToMock.class);
Mockito.when(ClassToMock.staticMethod()).thenReturn(expectedValue);

4 使用 PowerMockito.whenNew(ClassToMock.class) 方法来 Mock 构造函数:

PowerMockito.whenNew(ClassToMock.class).withArguments(argument1, argument2).thenReturn(mockInstance);

5 使用 PowerMockito.spy(mockInstance) 方法来创建一个 Spy 对象:

ClassToMock mockInstance = PowerMockito.spy(new ClassToMock());
Mockito.when(mockInstance.finalMethod()).thenReturn(expectedValue);

6 使用 PowerMockito.doCallRealMethod().when(mockInstance).nonFinalMethod() 方法来 Mock 非 final 方法:

PowerMockito.doCallRealMethod().when(mockInstance).nonFinalMethod();
Mockito.when(mockInstance.nonFinalMethod()).thenReturn(expectedValue);

7 在测试方法中,使用 PowerMockito.verifyStatic(ClassToMock.class) 方法来验证静态方法调用,使用 PowerMockito.verifyNew(ClassToMock.class) 方法来验证构造函数调用:

PowerMockito.verifyStatic(ClassToMock.class);
ClassToMock.staticMethod();

PowerMockito.verifyNew(ClassToMock.class).withArguments(argument1, argument2);

需要注意的是,Mock 静态方法和 final 方法可能会影响代码的可维护性和可读性,应该尽量避免使用它们。只有在必要时才使用它们,并且应该选择适当的 Mock 框架来保持代码的简洁性和可读性。

五、总结

Mockito是一个流行的Java模拟框架,它可以帮助开发人员编写单元测试,以便更好地验证代码的正确性。Mockito提供了一些常用的方法,例如模拟对象、测试桩、参数匹配、异步测试等,这些方法可以大大简化测试代码的编写和维护。Mockito的优点包括易学易用、广泛支持、文档丰富等,但也存在局限性,例如不支持mock final方法等。对于开发人员而言,使用Mockito进行单元测试可以提高代码质量,降低代码维护成本,是一个非常值得掌握的技能。​

猜你喜欢

转载自blog.csdn.net/bairo007/article/details/132634790