Test Double
Test Double

本部分主要介绍所谓的
public class OrderEasyTester extends TestCase {
private static String TALISKER = "Talisker";
private MockControl warehouseControl;
private Warehouse warehouseMock;
public void setUp() {
warehouseControl = MockControl.createControl(Warehouse.class);
warehouseMock = (Warehouse) warehouseControl.getMock();
}
public void testFillingRemovesInventoryIfInStock() {
//setup - data
Order order = new Order(TALISKER, 50);
//setup - expectations
warehouseMock.hasInventory(TALISKER, 50);
warehouseControl.setReturnValue(true);
warehouseMock.remove(TALISKER, 50);
warehouseControl.replay();
//exercise
order.fill(warehouseMock);
//verify
warehouseControl.verify();
assertTrue(order.isFilled());
}
public void testFillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER, 51);
warehouseMock.hasInventory(TALISKER, 51);
warehouseControl.setReturnValue(false);
warehouseControl.replay();
order.fill((Warehouse) warehouseMock);
assertFalse(order.isFilled());
warehouseControl.verify();
}
}
当我们进行单元测试的时候,我们会专注于软件中的一个小点,不过问题就是虽然我们只想进行一个单一模块的测试,但是不得不依赖于其他模块,就好像上面例子中的Test Double
这个术语来称呼这一类用于替换真实对象的模拟对象。
Dummy : 用于传递给调用者但是永远不会被真实使用的对象,通常它们只是用来填满参数列表。Fake : Fake 对象常常与类的实现一起起作用,但是只是为了让其他程序能够正常运行,譬如内存数据库就是一个很好的例子。Stubs : Stubs 通常用于在测试中提供封装好的响应,譬如有时候编程设定的并不会对所有的调用都进行响应。Stubs 也会记录下调用的记录,譬如一个email gateway 就是一个很好的例子,它可以用来记录所有发送的信息或者它发送的信息的数目。简而言之,Stubs 一般是对一个真实对象的封装。Mocks : Mocks 也就是Fowler 这篇文章讨论的重点,即是针对设定好的调用方法与需要响应的参数封装出合适的对象。
在上述这几种
public interface MailService {
public void send(Message msg);
}
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send(Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
然后就可以进行状态验证了:
class OrderStateTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1, mailer.numberSent());
}
当然这是一个非常简单的测试,我们并没有测试它是否发给了正确的人或者发出了正确的内容。而如果使用
class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
}
}
在两个例子中我们都是用了
测试中Fakes 、Mocks 以及Stubs 概念明晰
自动化测试中,我们常会使用一些经过简化的,行为与表现类似于生产环境下的对象的复制品。引入这样的复制品能够降低构建测试用例的复杂度,允许我们独立而解耦地测试某个模块,不再担心受到系统中其他部分的影响;这类型对象也就是所谓的
Fake
Fakes are objects that have working implementations, but not same as production one. Usually they take some shortcut and have simplified version of production code.
Fake 是那些包含了生产环境下具体实现的简化版本的对象。
如下图所示,

@Profile("transient")
public class FakeAccountRepository implements AccountRepository {
Map<User, Account> accounts = new HashMap<>();
public FakeAccountRepository() {
this.accounts.put(new User("john@bmail.com"), new UserAccount());
this.accounts.put(new User("boby@bmail.com"), new AdminAccount());
}
String getPasswordHash(User user) {
return accounts.get(user).getPasswordHash();
}
}
除了应用到测试,
Stub
Stub is an object that holds predefined data and uses it to answer calls during tests. It is used when we cannot or don’t want to involve objects that would answer with real data or have undesirable side effects.
Stub 代指那些包含了预定义好的数据并且在测试时返回给调用者的对象。Stub 常被用于我们不希望返回真实数据或者造成其他副作用的场景。

public class GradesService {
private final Gradebook gradebook;
public GradesService(Gradebook gradebook) {
this.gradebook = gradebook;
}
Double averageGrades(Student student) {
return average(gradebook.gradesFor(student));
}
}
我们在编写测试用例时并没有从
public class GradesServiceTest {
private Student student;
private Gradebook gradebook;
@Before
public void setUp() throws Exception {
gradebook = mock(Gradebook.class);
student = new Student();
}
@Test
public void calculates_grades_average_for_student() {
when(gradebook.gradesFor(student)).thenReturn(grades(8, 6, 10)); //stubbing gradebook
double averageGrades = new GradesService(gradebook).averageGrades(student);
assertThat(averageGrades).isEqualTo(8.0);
}
}
Command Query Separation
仅返回部分结果而并没有真实改变系统状态的的方法被称作查询avarangeGrades
,用于返回学生成绩平均值的函数就是非常典型的例子:Double getAverageGrades(Student student);
。该函数仅返回了某个值,而没有其他的任何副作用。正如我们上文中介绍的,我们可以使用
Mock
Mocks are objects that register calls they receive. In test assertion we can verify on Mocks that all expected actions were performed.
Mocks 代指那些仅记录它们的调用信息的对象,在测试断言中我们需要验证Mocks 被进行了符合期望的调用。
当我们并不希望真的调用生产环境下的代码或者在测试中难于验证真实代码执行效果的时候,我们会用

public class SecurityCentral {
private final Window window;
private final Door door;
public SecurityCentral(Window window, Door door) {
this.window = window;
this.door = door;
}
void securityOn() {
window.close();
door.close();
}
}
在上述代码中,我们并不想真的去关门来测试
public class SecurityCentralTest {
Window windowMock = mock(Window.class);
Door doorMock = mock(Door.class);
@Test
public void enabling_security_locks_windows_and_doors() {
SecurityCentral securityCentral = new SecurityCentral(windowMock, doorMock);
securityCentral.securityOn();
verify(doorMock).close();
verify(windowMock).close();
}
}
在 securityOn
方法执行之后,SecurityCentral
这个类关注的目标。门与窗是否能被正常的关闭应该是由 Door
与 Window
这两个类所关注的。