本文内容主要翻译自:https://adamcod.es/2014/05/15/test-doubles-mock-vs-stub.html
怎么翻译Test Double本身是个问题,网上已经有人使用“测试替身”,就不自己另发明了。为什么叫测试替身,而不是Mock或者Stub?因为作者发现混用的现象非常严重。这篇文章对测试替身做了详细的解释与分类。
Dummies
Dummy是傀儡的意思。Dummy只是一个接口的实现,没有做任何其他的事情。它不会在测试中使用,也不会影响代码的行为。举个例子:
private class FooDummy implements Foo
{
public String bar() { return null; }
}
public class FooCollectionTest
{
@Test
public void it_should_maintain_a_count()
{
FooCollection sut = new FooCollection();
sut.add(new FooDummy);
sut.add(new FooDummy);
assertEquals(2, sut.count());
}
}
FooCollectionTest是用来测试Foo容器的。它并不关心Foo对象本身的方法bar,因此我们构造了一个FooDummy类。可以简单说,Dummy是用来解决对象使用时的编译问题。
Stubs
Stub可以认为是Dummy的增强版。它不像Dummy没有任何实际方法体,Stub一般会返回一些预先设置的数据。使用Stub可以测试中做些有用的检查。举个例子:
private class FooStub implements Foo
{
public String bar()
{
return "baz";
}
}
public class FooCollectionTest
{
@Test
public void it_should_return_joined_bars()
{
FooCollection sut = new FooCollection();
sut.add(new FooStub);
sut.add(new FooStub);
assertEquals("bazbaz", sut.joined());
}
}
Foo容器的joined函数用来连接容器中的Foo对象。为了达到测试目的,bar直接返回空,或者一样的字符(比如“aa”)就不能达到测试的目的。因此我们让bar作为一个Stub返回“baz”,这样连接出来的结果就有意义了。
Spies
如果一个Stub维护了内部状态,并用来做断言检查。那么它就升级成为了Spy(一个骗子)。举个例子:
private class ThirdPartyApiSpy implements ThirdPartyApi
{
public int callCount = 0;
public boolean hasMore(Response previousResponse)
{
if (this.callCount == 0) {
return true;
}
return false;
}
public Response get(int page)
{
this.callCount++;
return new DummyResponse;
}
}
public class ApiConsumerTest
{
@Test
public void it_should_get_all_pages()
{
ThirdPartyApiSpy spy = new ThirdPartyApiSpy
ApiConsumer sut = new ApiConsumer(spy);
sut.fetchAll()
assertEquals(2, spy.callCount);
}
}
Fakes
可以理解Fake是Stub的升级版。它不仅仅只是给一些返回值,它能够像真实的对象一样与测试对象进行交互。举个例子,为了持久化存储数据,我们将数据写入文件或者数据库。但是单元测试肯定不能这样做,我们可以构造一个内存对象,让它模拟实现写入或者数据库。
private class InMemoryUserRepository implements UserRepository
{
private UserCollection users = new UserCollection;
public User load(UserIdentifier identifier)
{
if (!this.users.exists(identifier)) {
throw new InvalidUserException;
}
return this.users.get(identifier);
}
public User find(UserIdentifier identifier)
{
if (!this.users.exists(identifier)) {
return null;
}
return this.users.get(identifier);
}
public UserCollection fetchAll()
{
return this.users;
}
public boolean add(User user)
{
return this.users.add(user);
}
public boolean delete(User user)
{
return this.delete(user.getIdentifier());
}
public boolean delete(UserIdentifier identifier)
{
return this.users.remove(identifier);
}
}
public class CreateUserServiceTest
{
@Test
public void it_should_save_a_new_user()
{
UserRepository userRepository = new InMemoryUserRepository;
CreateUserService sut = new CreateUserService(userRepository);
sut.createUser(new UserRequestStub);
assertEquals(new UserCollectionStub, userRepository.fetchAll());
}
}
这里省略了UserRequestStub和UserCollectionStub,实现它们也比较简单。InMemoryUserRepository像一个真实的UserRepository对象。你能够添加删除用户,搜索用户,加载用户。唯一的问题是它不能真正的做持久话,所有它不能作为产品代码,只能是测试代码。
Mocks
前面提到的几种测试替身都比较近似。至少测试能力或多或少,但Mock则完全不同了。前面几种测试替身都是基于状态(state)进行断言的,而Mock是基于行为(Behavior)进行断言的。Mock会说“为期望你调用foo()并且携带参数bar,如果没有达到期望,将会报错!”
使用Mock一般分为三步:
创建对象例
定义行为
断言调用符合期望
举个例子:
public class FooCollectionTest
{
@Test
public void it_should_return_joined_bars()
{
Foo fooMock = mock(Foo.class); // instance
when(fooMock.bar()).thenReturn("baz", "qux"); // behaviour
FooCollection sut = new FooCollection();
sut.add(fooMock);
sut.add(fooMock);
assertEquals("bazqux", sut.joined());
verify(fooMock, times(2)).bar(); // verify
}
}
如果我们撇开verify()不看,这个测试替身依然是Stub。这里关键的不同点是verify()。Stub只是断言状态,而Mock断言的是正确方法被调用正确的次数(也包括方法调用的顺序和方法调用使用的参数)。