Post

Mockito使用教程

1.简介

Mockito是一个用于Java单元测试的mock框架,用于创建模拟对象(mock object)来替代真实对象,帮助开发者隔离外部依赖,从而专注于单元测试的逻辑。

其他常见的Java mock框架有jMockEasyMock

2.声明依赖

Maven依赖:

1
2
3
4
5
6
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.11.0</version>
    <scope>test</scope>
</dependency>

Gradle依赖:

1
testImplementation 'org.mockito:mockito-core:4.11.0'

Mockito通常配合单元测试框架(如JUnit)使用。

3.基本用法

Mockito的核心功能包括:

  • 创建mock对象:使用mock()创建mock对象。
  • 打桩:使用when()thenReturn()等方法指定mock对象的特定方法被调用时的行为(如返回值或抛出异常)。
  • 验证行为:使用verify()检查mock对象的特定方法是否被调用,参数和调用次数是否符合预期。

下面通过示例介绍Mockito的基本用法。

完整代码:ListTest

3.1 验证行为

下面的示例mock一个List对象,使用该mock对象,并验证方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Let's import Mockito statically so that the code looks clearer
import static org.mockito.Mockito.*;

// mock creation
List mockList = mock(List.class);

// using mock object
mockList.add("one");
mockList.clear();

// verification
verify(mockList).add("one");
verify(mockList).clear();

org.mockito.Mockito类的mock()方法用于创建指定类或接口的mock对象。一旦创建,mock对象就会记住所有的方法调用。之后可以选择性地验证感兴趣的方法调用。

verify()方法用于验证行为:verify(mock).someMethod(args...)验证“mock对象的someMethod()方法使用给定的参数被调用过恰好一次”。如果该方法没有被调用过,或者参数不匹配,测试将会失败。

3.2 打桩

打桩(stubbing)即指定mock对象的方法被调用时的行为(如返回值、抛出异常等)。语法为:

1
when(mock.someMethod(args...)).thenReturn(value)

表示“当mock对象的someMethod()使用给定的参数被调用时应该返回value”。也可以使用thenThrow()方法指定应该抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class);

// stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

// following returns "first"
assertEquals("first", mockedList.get(0));

// following throws runtime exception
assertThrows(RuntimeException.class, () -> mockedList.get(1));

// following returns "null" because get(999) was not stubbed
assertNull(mockedList.get(999));

// Although it is possible to verify a stubbed invocation, usually it's just redundant
// If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
// If your code doesn't care what get(0) returns, then it should not be stubbed.
verify(mockedList).get(0);
  • 默认情况下,对于所有返回值的方法,mock对象将返回适当的默认值。例如,对于intInteger返回0,对于booleanBoolean返回false,对于集合类型返回空集合,对于其他对象类型(例如字符串)返回null
  • 打桩可以被覆盖。当多次使用相同的参数打桩同一个方法时,只有最后一次打桩有效。例如,打桩通常在@Before方法中,但测试方法可以覆盖它。
  • 一旦被打桩,方法将返回指定的值,无论调用多少次。

3.3 参数匹配器

Mockito默认使用equals()方法验证参数值。当需要额外的灵活性时,可以使用参数匹配器。verify()when()中的方法参数都可以使用匹配器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// stubbing using built-in anyInt() argument matcher
when(mockedList.get(anyInt())).thenReturn("element");

// stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
when(mockedList.contains(argThat(isValid()))).thenReturn(true);

// following returns "element"
assertEquals("element", mockedList.get(999));

// you can also verify using an argument matcher
verify(mockedList).get(anyInt());

// argument matchers can also be written as Java 8 Lambdas
mockedList.add("element");
verify(mockedList).add(argThat(s -> s.length() > 5));

更多的内置参数匹配器参见ArgumentMatchersMockitoHamcrest

注:Mockito继承了ArgumentMatchers,因此可以直接通过Mockito类使用这些参数匹配器。

注意:

  • 要自定义参数匹配器,需要继承ArgumentMatcher接口(可以直接使用lambda表达式),并用argThat()包装。例如,anyInt()大致等价于argThat(x -> x instanceof Integer)
  • 最好不要使用复杂的参数匹配,而是实现equals()方法来帮助测试。
  • ArgumentCaptor是一种特殊的参数匹配器,可以捕获参数值用于进一步断言。详见3.12节。
  • 如果使用参数匹配器,则所有参数都必须由匹配器提供。例如:
1
2
3
4
5
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// above is correct - eq() is also an argument matcher

verify(mock).someMethod(anyInt(), anyString(), "third argument");
// above is incorrect - exception will be thrown because third argument is given without an argument matcher.

3.4 验证调用次数

可以使用verify()方法的第二个参数指定期望的调用次数,可以是times(n)atLeastOnce()never()等。

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
// using mock
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

// following two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");

// exact number of invocations verification
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

// verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened");

// verification using atLeast()/atMost()
verify(mockedList, atMostOnce()).add("once");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");

times(1)是默认的,因此可以省略。

3.5 打桩void方法

打桩void方法需要一种不同于when()的方式。如果希望void方法抛出异常,则使用doThrow()

1
2
3
4
doThrow(new RuntimeException()).when(mockedList).clear();

// following throws RuntimeException:
assertThrows(RuntimeException.class, mockedList::clear);

可以使用doThrow()代替when().thenThrow(),但在下列情况下是必需的:

  • 打桩void方法
  • 打桩spy对象的方法(见3.11节)
  • 多次打桩同一个方法,在测试期间改变mock的行为

另见doReturn()doAnswer()doNothing()doCallRealMethod()

3.6 按顺序验证

使用inOrder()验证mock方法的调用顺序。

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
// A. Single mock whose methods must be invoked in a particular order
List<String> singleMock = mock(List.class);

// using a single mock
singleMock.add("was added first");
singleMock.add("was added second");

// create an inOrder verifier for a single mock
InOrder inOrder = inOrder(singleMock);

// following will make sure that add is first called with "was added first", then with "was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");

// B. Multiple mocks that must be used in a particular order
List<String> firstMock = mock(List.class);
List<String> secondMock = mock(List.class);

// using mocks
firstMock.add("was called first");
secondMock.add("was called second");

// create inOrder object passing any mocks that need to be verified in order
InOrder inOrder = inOrder(firstMock, secondMock);

// following will make sure that firstMock was called before secondMock
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");

按顺序验证是灵活的,不必逐一验证所有调用,只需按顺序验证感兴趣的那些即可。

3.7 验证多余的调用

使用verifyNoMoreInteractions()检查mock对象没有未验证的调用。

1
2
3
4
5
6
7
8
// using mocks
mockedList.add("one");
mockedList.add("two");

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

// following verification will fail
verifyNoMoreInteractions(mockedList);

由于mockedList.add("two")被调用过,但没有验证,因此最后的测试将会失败。

注意:“期望-运行-验证”式mock(例如jMock和EasyMock)假定只存在期望的调用,相当于在每个测试末尾自动添加verifyNoMoreInteractions(),这导致不得不验证一些不感兴趣的调用。Mockito属于“打桩-运行-验证”式mock,不存在“期望”。因此不推荐过多地使用verifyNoMoreInteractions(),只需验证感兴趣的那些调用即可。另外,可以使用never()验证预期不存在的调用(见3.4节)。

3.8 @Mock注解

可以使用@Mock注解将字段标记为mock对象,从而减少创建mock的重复代码,使测试类更易读。例如:

1
2
3
4
5
6
7
8
9
10
11
@RunWith(MockitoJUnitRunner.class)
public class ListTest {
    @Mock
    private List<String> myMockedList;

    @Test
    public void mockAnnotation() {
        myMockedList.add("foo");
        verify(myMockedList).add("foo");
    }
}

注意:该注解需要使用MockitoJUnitRunner运行器或MockitoRule规则。

3.9 打桩连续调用

可以对同一个方法调用打桩指定不同的返回值/异常。

1
2
3
4
5
6
7
8
9
10
11
12
when(mockedList.get(0))
    .thenThrow(new RuntimeException())
    .thenReturn("foo");

// First call: throws runtime exception:
assertThrows(RuntimeException.class, () -> mockedList.get(0));

// Second call: returns "foo"
assertEquals("foo", mockedList.get(0));

// Any consecutive call: returns "foo" as well (last stubbing wins).
assertEquals("foo", mockedList.get(0));

第一次调用mockedList.get(0)抛出RuntimeException,第二次及后续调用都返回"foo"

也可以使用thenReturn()指定多个返回值。

1
2
3
4
5
6
7
when(mockedList.get(1))
    .thenReturn("one", "two", "three");

assertEquals("one", mockedList.get(1));
assertEquals("two", mockedList.get(1));
assertEquals("three", mockedList.get(1));
assertEquals("three", mockedList.get(1));

前三次调用mockedList.get(1)分别返回"one""two""three",后续调用都返回"three"

警告:如果多次打桩同一个方法调用,前面的打桩将被覆盖。

1
2
3
4
5
// All mock.someMethod("some arg") calls will return "two"
when(mock.someMethod("some arg"))
    .thenReturn("one");
when(mock.someMethod("some arg"))
    .thenReturn("two");

3.10 回调打桩

通过thenAnswer()(或者别名then())可以使用泛型接口Answer进行打桩。

1
2
3
4
5
6
when(mockedList.set(anyInt(), anyString())).thenAnswer(invocation -> {
    Object[] args = invocation.getArguments();
    return "called with arguments: " + Arrays.toString(args);
});

assertEquals("called with arguments: [1, foo]", mockedList.set(1, "foo"));

3.11 spy对象

可以使用spy()创建真实对象的间谍(spy)。使用spy对象时,将会调用真实的方法,除非该方法被打桩(即“部分mock”)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<String> list = new LinkedList<>();
List<String> spy = spy(list);

// optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);

// using the spy calls *real* methods
spy.add("one");
spy.add("two");

// returns "one" - the first element of a list
assertEquals("one", spy.get(0));

// size() method was stubbed - 100 is returned
assertEquals(100, spy.size());

// optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");

重要提示

  • 有时无法使用when()打桩spy对象(例如真实方法会抛出异常),此时可以使用doReturn()doThrow()等方法(见3.5节)。
1
2
3
4
5
6
7
8
9
10
List<String> list = new LinkedList<>();
List<String> spy = spy(list);

// Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
// when(spy.get(0)).thenReturn("foo");

// You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);

assertEquals("foo", spy.get(0));
  • Mockito 不会将调用传递给真实对象,而是创建了一个副本。因此,在spy对象上调用未打桩的方法不会影响真实对象。
  • 当心final方法。Mockito不会打桩final方法,也无法验证这些方法。

3.12 捕获参数

在某些情况下,在验证之后对具体参数值进行断言是有帮助的。例如:

1
2
3
4
5
6
List<Person> mockedList = mock(List.class);
mockedList.add(new Person("John", 30));

ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
verify(mockedList).add(argument.capture());
assertEquals("John", argument.getValue().getName());

推荐将ArgumentCaptor用于验证而不是打桩,因为会降低测试的可读性。通过ArgumentMatcher自定义参数匹配器通常更适合于打桩(见3.3节)。

4.MVC示例

在真实项目的测试中使用Mockito通常包括以下步骤。假设类A依赖类B,要对类A进行测试:

  1. 创建类B的mock对象。
  2. (可选)打桩mock对象的方法。
  3. 将mock对象传递给类A
  4. 调用被测方法。
  5. 验证被测方法的结果。
  6. 验证mock对象的调用。

注意:第3步要求被测类通过构造器参数或者setter方法传递依赖对象,而不是直接在方法中创建。这就是依赖注入的基本思想。

下面通过一个MVC应用的示例进行说明。

实体类User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class User {
    private int id;
    private String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

DAO接口UserRepository

1
2
3
4
public interface UserRepository {
    User findById(int id);
    void save(User user);
}

Service类UserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String getUsername(int id) {
        User user = userRepository.findById(id);
        return user != null ? user.getName() : null;
    }

    public void saveUser(User user) {
        userRepository.save(user);
    }
}

Service测试:

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
public class UserServiceTest {
    @Test
    public void testGetUsername() {
        // create mock
        UserRepository mockRepository = mock(UserRepository.class);

        // stubbing
        when(mockRepository.findById(1)).thenReturn(new User(1, "Alice"));

        // use mock
        UserService userService = new UserService(mockRepository);
        String username = userService.getUsername(1);

        // assert result
        assertEquals("Alice", username);

        // verify invocation
        verify(mockRepository).findById(1);
    }

    @Test
    public void testSaveUser() {
        UserRepository mockRepository = mock(UserRepository.class);

        UserService userService = new UserService(mockRepository);
        User user = new User(2, "Bob");
        userService.saveUser(user);

        ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
        verify(mockRepository).save(captor.capture());

        assertEquals(2, captor.getValue().getId());
        assertEquals("Bob", captor.getValue().getName());
    }
}

完整代码:UserServiceTest

This post is licensed under CC BY 4.0 by the author.