理解测试替身(Test Double)Mock VS Stub

本文内容主要翻译自: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一般分为三步:

  1. 创建对象例

  2. 定义行为

  3. 断言调用符合期望

举个例子:

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断言的是正确方法被调用正确的次数(也包括方法调用的顺序和方法调用使用的参数)。


你可能感兴趣的:(理解测试替身(Test Double)Mock VS Stub)