java mock

Mock 测试

Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。

比如一段代码有这样的依赖:

img

当我们需要测试A类的时候,如果没有 Mock,则我们需要把整个依赖树都构建出来,而使用 Mock 的话就可以将结构分解开,像下面这样:

img

mock对象就是在调试期间用来作为真实对象的替代品。

mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试。

Mock 对象使用范畴

  • 真实对象具有不可确定的行为,产生不可预测的效果,(如:股票行情,天气预报)
  • 真实对象很难被创建的
  • 真实对象的某些行为很难被触发
  • 真实对象实际上还不存在的(和其他开发小组或者和新的硬件打交道)等等

Mockito 使用

声明 mockito 依赖

1
2
3
4
5
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.0.111-beta</version>
</dependency>

示例

1. 验证行为

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 mockedList = mock(List.class);
//using mock object
mockedList.add("one");
mockedList.clear();
//verification
verify(mockedList).add("one");
verify(mockedList).clear();

一旦创建 mock 将会记得所有的交互。你可以选择验证你感兴趣的任何交互

2. stubbing

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 prints "first"
System.out.println(mockedList.get(0));
//following throws runtime exception
System.out.println(mockedList.get(1));
//following prints "null" because get(999) was not stubbed
System.out.println(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. Not convinced? See here.
verify(mockedList).get(0);
  • 默认情况下,所有方法都会返回值,一个 mock 将返回要么 null,一个原始/基本类型的包装值或适当的空集。例如,对于一个 int/Integer 就是 0,而对于 boolean/Boolean 就是 false。
  • Stubbing 可以被覆盖。
  • 一旦 stub,该方法将始终返回一个 stub 的值,无论它有多少次被调用。
  • 最后的 stubbing 是很重要的 - 当你使用相同的参数 stub 多次同样的方法。换句话说:stubbing 的顺序是重要的,但它唯一有意义的却很少,例如当 stubbing 完全相同的方法调用,或者有时当参数匹配器的使用,等等。

3. 参数匹配器

Mockito 验证参数值使用 Java 方式:通过使用 equals() 方法。有时,当需要额外的灵活性,可以使用参数匹配器:

1
2
3
4
5
6
7
8
9
10
11
//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("element");
//following prints "element"
System.out.println(mockedList.get(999));
//you can also verify using an argument matcher
verify(mockedList).get(anyInt());

下面的示例演示验证,但同样适用于 stubbing:

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.

4. 调用额外的调用数字/at least / 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
//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, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");

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

5. Stubbing void 方法处理异常

1
2
3
4
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();

6. 有序的验证

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
// A. Single mock whose methods must be invoked in a particular order
List 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 firstMock = mock(List.class);
List 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");
// Oh, and A + B can be mixed together at will

有序验证是为了灵活 - 不必一个接一个验证所有的交互。

此外,还可以通过创建 InOrder 对象传递只与有序验证相关的 mock 。

7. 确保 mock 上不会发生交互

1
2
3
4
5
6
7
8
9
10
11
//using mocks - only mockOne is interacted
mockOne.add("one");
//ordinary verification
verify(mockOne).add("one");
//verify that method was never called on a mock
verify(mockOne, never()).add("two");
//verify that other mocks were not interacted
verifyZeroInteractions(mockTwo, mockThree);

8. 寻找多余的调用

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);

注意:不建议 verifyNoMoreInteractions() 在每个测试方法中使用。 verifyNoMoreInteractions() 是从交互测试工具包一个方便的断言。只有与它的相关时才使用它。滥用它导致难以维护。

9. 标准创建 mock 方式 - 使用 @Mock 注解

  • 最小化可重用 mock 创建代码
  • 使测试类更加可读性
  • 使验证错误更加易读,因为字段名称用于唯一识别 mock

public class ArticleManagerTest {

1
2
3
4
5
@Mock private ArticleCalculator calculator;
@Mock private ArticleDatabase database;
@Mock private UserProvider userProvider;
private ArticleManager manager;

在基础类或者测试 runner 里面,使用如下:

1
MockitoAnnotations.initMocks(testClass);

可以使用内建 runner: MockitoJUnitRunner 或者 rule: MockitoRule

更多详见 MockitoAnnotations

10. Stubbing 连续调用(迭代器式的 stubbing)

1
2
3
4
5
6
7
8
9
10
11
12
when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");
//First call: throws runtime exception:
mock.someMethod("some arg");
//Second call: prints "foo"
System.out.println(mock.someMethod("some arg"));
//Any consecutive call: prints "foo" as well (last stubbing wins).
System.out.println(mock.someMethod("some arg"));

下面是一个精简版本:

1
2
when(mock.someMethod("some arg"))
.thenReturn("one", "two", "three");

11. 回调 Stubbing

允许使用泛型 Answer 接口。

然而,这是不包括在最初的 Mockito 另一个有争议的功能。建议只需用thenReturn() 或 thenThrow() 来 stubbing ,这在测试/测试驱动中应用简洁与简单的代码足够了。但是,如果有一个需要 stub 到泛型 Answer 接口,这里是一个例子:

1
2
3
4
5
6
7
8
9
10
when(mock.someMethod(anyString())).thenAnswer(new Answer() {
Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + args;
}
});
//the following prints "called with arguments: foo"
System.out.println(mock.someMethod("foo"));

案例:

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
@Component
public class SalaryService {
    private static final BigDecimal minimumSalary = new BigDecimal(20000);
    @Autowired
    private EmployeeDAO employeeDAO;
    @Autowired
    private TaxCalculator taxCalculator;
    public BigDecimal getNetSalary(long employeeId) {
        BigDecimal netSalary = null;
        BigDecimal grossSalary = employeeDAO.getAnnualSalary(employeeId);
        BigDecimal taxes = taxCalculator.calculateTaxes(grossSalary);
        if (taxedSalaryIsGreaterThanMinimumSalary(grossSalary)) {
            netSalary = grossSalary.subtract(taxes);
        } else {
            netSalary = grossSalary;
        }
        return netSalary;
    }
    private boolean taxedSalaryIsGreaterThanMinimumSalary(BigDecimal taxedSalary) {
        return taxedSalary.compareTo(minimumSalary) == 1;
    }
}
1
2
3
4
5
6
7
8
9
@Component
public class EmployeeDAO {
public BigDecimal getAnnualSalary(long employeeId) {
// conncetTODB
// run select for employeeId;
return new BigDecimal(70000);
}
}
1
2
3
4
5
6
7
8
9
@Component
public class TaxCalculator {
public BigDecimal calculateTaxes(BigDecimal salary) {
BigDecimal result = salary.multiply(new BigDecimal(200));
// some other weird calculation ....
return result;
}
}

测试案例:

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
public class SalaryServiceTest {
private static final long UserId = 123l;
@InjectMocks
private SalaryService salaryService;
@Mock
private EmployeeDAO employeeDAO;
@Mock
private TaxCalculator taxCalculator;
@Before
public void init() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testMinimumSalary() {
BigDecimal annualSalary = new BigDecimal(10000);
Mockito.when(employeeDAO.getAnnualSalary(UserId)).thenReturn(annualSalary);
Mockito.when(taxCalculator.calculateTaxes(annualSalary)).thenReturn(new BigDecimal(1000));
BigDecimal actual = salaryService.getNetSalary(UserId);
assertThat(actual.compareTo(new BigDecimal(10000)), is(0));
}
@Test
public void testMaximumSalary() {
BigDecimal annualSalary = new BigDecimal(80000);
Mockito.when(employeeDAO.getAnnualSalary(UserId)).thenReturn(annualSalary);
Mockito.when(taxCalculator.calculateTaxes(annualSalary)).thenReturn(new BigDecimal(8000));
BigDecimal actual = salaryService.getNetSalary(UserId);
assertThat(actual.compareTo(new BigDecimal(72000)), is(0));
}
}

参考

Mockito

热评文章