什么是Test Doubles
In software testing, we developed unit tests and integration tests to test the code's functionality. However, in the real world, it is very common for a piece of code to interact with external components, for example, databases, public APIs, or other computers. n the real world with distributed computing, challenges are everywhere, systems will have to handle things like black Friday user traffic, lightning hit the data centers. To build robust software, our tests should cover all possible cases. As behaviors of external dependencies are hard to control, test doubles are introduced to replace production objects to reduce complexity and facilitate testing. Test doubles are useful not only to test unlikely events, but also isolates normal tests from the unpredictability of the real system.
"Test Doubles" 是一种用来辅助单元测试的技术,这个名字是由Gerard Meszaros首次提出的,他将其比喻为电影制作中的替身演员。当我们进行单元测试时,我们通常关注的是测试特定的函数或方法。然而,这些函数或方法常常依赖于其他的对象或系统。例如,一个函数可能需要从数据库中查询数据,或者需要调用另一个服务的API。在这种情况下,这些依赖关系可能会导致测试变得复杂或者不可预测。为了解决这个问题,我们可以使用"Test Doubles"来模拟这些依赖关系。
Types of Test Doubles
"Test Doubles"的类型主要有以下几种:Fakes, Stubs, Mocks。
Fakes
Fakes 是真实对象的简化版本。它们是完全功能性的,但是以一种更简单或者更容易控制的方式实现。例如,你可能会创建一个 Fake 的数据库,这个数据库可能只是在内存中存储数据,而不是在硬盘上。
Stubs
Stubs 是预先配置了返回数据的人工类。它们并不关心被调用了多少次或者以什么参数被调用,它们只是简单地返回预先配置的响应。例如,你可能会创建一个 Stub 的网络服务,这个服务可能总是返回固定的响应,而不管请求的内容是什么。
可以说stub比fake还要省事儿,直接预定好返回值,连装都不装了。
Mocks
Mocks 是真实类的变种,可以进行细粒度的控制。与 Stubs 不同,Mocks 可以检查它们被调用了多少次,以及被调用时的参数是什么。Mocks 通常用于验证你的代码是否正确地与依赖关系进行交互。例如,在电子邮件服务中,我们不希望每次运行测试时都发送电子邮件。此外,在测试中验证是否发送了正确的电子邮件并不容易。我们唯一能做的就是验证 sendEmail() 方法是否被调用。我们可以为 sendEmail() 写一个 mock 方法,这个 mock 方法将帮助我们记录它是否被我们的应用程序调用。这样,我们就可以在不实际发送电子邮件的情况下,验证我们的代码是否正确地调用了发送电子邮件的方法。
使用Test Doubles的其他好处
第三方API可能出现故障并不是我们使用test doubles的唯一原因。使用测试替身还有这些好处:
- 速度/性能:API可能很慢(网络流量、大型数据库等)。良好的测试套件能快速执行,这使得我们可以进行更多的测试场景。
- 覆盖率高:例如API的更改,响应缓慢,超时,出错等。我们都可以通过mock模拟出来。
- 稳定性:确保返回结果的确定性,减少测试的不稳定性。
- 用于开发:假设我们还没有实现的功能,我们也可以先用Test double替代它,此时test double的作用就不是测试而是开发了,也未尝不可。
Java中的Test Doubles实践
1 Fakes的创建
Fakes的创建很简单也很好理解。我们其实只需要创建一个新的类,这个类应该实现与原始类相同的接口。这样子我们就有了这个“替身”来代替原始类了。然后我们在这个类写相应的工作实现,但相比原始实现,它应该更简洁、更快。
2 Stubs and Mocks的创建
创建Stubs和Mocks我们可以用一个叫Mockito的Java框架。
2.1 一个使用Mockito创建stub的例子:
// 1 - create mock object
LinkedList mockedList = mock(LinkedList.class);
// 2 - define stubbing return value before actual execution
when(mockedList.get(0)).thenReturn("first");
// 3 - call methods
// 3.1 - the following prints "first"
System.out.println(mockedList.get(0));
// 3.2 - the following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
在这个例子中我们提前定义好了如果调用get(0)就返回“first”,对于没预定义的会自动返回null。
下面是一个用回调函数创建stub的例子:
when(mock.someMethod(anyString())).thenAnswer(
new Answer() {
public Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + Arrays.toString(args);
}
});
when
函数可以接受一个Answer
对象作为参数,这个Answer
对象定义了当预期的方法被调用时,应该如何响应。这个例子中,当someMethod
函数被调用时,我们返回了一个包含了所有参数的字符串。
Mockito提供了一系列的doXxx
函数,比如doThrow()
, doAnswer()
, doNothing()
, doReturn()
和doCallRealMethod()
,这些函数可以用来代替when
函数,用于定义当预期的方法被调用时的行为。
2.2 一个使用Mockito创建Mock的例子
// 1 - create mock object
List mockedList = mock(List.class);
// 2 - call methods on mock object
mockedList.add("one");
mockedList.clear();
// 3 - verify the methods are called
verify(mockedList).add("one");
verify(mockedList).clear();
// 3.1 - verify the number of times a method is called
verify(mockedList, times(1)).add("one");
我们可以看到除了验证方法是不是调用过,还可以看方法被调用的次数,这正是Mock的功能特点。在这个代码中,假设add("one")
方法并没有被调用过,当你执行verify(mockedList).add("one")
,Mockito 就会抛出一个异常,说明这个预期的调用并没有发生。如果add("one")
方法被正确地调用过,那么verify
方法就不会有任何动作,测试将继续进行。
除了times,verify还可以接受其他参数,例如,never()
表示预期从未调用,atMostOnce()
表示最多调用一次,atLeast(2)
表示至少调用两次。如果实际的调用次数与预期不符,verify
会抛出异常。
verify(mockedList, never()).add("never happened");
verify(mockedList, atMostOnce()).add("once");
verify(mockedList, atLeast(2)).add("three times");
Reference 最后附一个英文参考:
https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da