在自动化测试中,通常使用外观和行为类似于其生产等效物的对象,但实际上对其进行了简化。这降低了复杂性,允许独立于系统其余部分来验证代码,有时甚至完全有必要执行自我验证测试。Test Double是用于这些对象的通用术语。
尽管双重测试有多种方式(Gerard Meszaros在本文中介绍了五种类型),但人们倾向于使用术语“Mock ”来指代不同种类的双重测试。误解和混合测试加倍实施可能会影响测试设计并增加测试的易碎性,从而阻碍了无缝重构的发展。
在本文中,我将介绍双重测试的三种实现变体:Fake,Stub和Mock,并提供使用它们的示例。
fack
fack具有有效的实现,但与生产实现不同。通常,他们采取一些捷径并简化了生产代码版本。
此快捷方式的一个示例可以是数据访问对象或存储库的内存中实现。这个fack实现不会使用数据库,但是会使用一个简单的集合来存储数据。这使我们可以对服务进行集成测试,而无需启动数据库和执行耗时的请求。
@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();
}
}
除了测试之外,fack实现还可以方便地进行原型设计和峰值测试。我们可以使用内存中存储快速实现并运行我们的系统,从而推迟有关数据库设计的决策。另一个示例也可以是fack付款系统,该系统将始终返回成功的付款。
stub
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));
}
}
与其从Gradebook商店中调用数据库来获取真实的学生成绩,我们不为stub预先配置要返回的成绩。我们只定义了足够的数据来测试平均计算算法。
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);
}
}
命令查询分离
返回某些结果并且不更改系统状态的方法称为Query。方法averageGrades返回平均学生成绩是一个很好的例子。
Double averageGrades(Student student);
它返回一个值,并且没有副作用。正如我们在学生评分示例中看到的那样,为了测试这种类型的方法,我们使用stub。我们正在替换实际功能,以提供方法执行其工作所需的值。然后,该方法返回的值可用于断言。
还有另一类方法称为Command。这是一种方法执行某些操作时会更改系统状态的情况,但是我们不希望从中获得任何返回值。
void sendReminderEmail(Student student);
好的作法是将对象的方法分为两个单独的类别。
这种做法被命名为:Bertrand Meyer在他的书《面向对象的软件构造》中的命令查询分离。
为了测试查询类型方法,我们应该优先使用stub,因为我们可以验证方法的返回值。但是,命令类型的方法(例如发送电子邮件的方法)又如何呢?当它们不返回任何值时如何测试它们?答案是Mock -我们要介绍的最后一种测试假人。
mock
mock是记录他们收到的呼叫的对象。
在测试断言中,我们可以在Mocks上验证是否已执行所有预期的操作。
当我们不想调用生产代码或没有简单的方法来验证目标代码是否已执行时,可以使用Mock 。没有返回值,也没有简便的方法来检查系统状态更改。一个示例可以是调用电子邮件发送服务的功能。
我们不想每次运行测试都发送电子邮件。此外,在测试中验证发送了正确的电子邮件并不容易。我们唯一能做的就是验证测试中所执行功能的输出。在其他情况下,请验证是否已调用电子邮件发送服务。
下例显示了类似的情况:
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();
}
}
我们不想关闭真实的门来测试安全性方法是否有效,对吗?相反,我们将门和窗Mock 对象放置在测试代码中。
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方法后,窗和门Mock 记录了所有交互。这使我们可以验证是否指示了窗户和门对象自行关闭。这就是我们需要从SecurityCental角度进行测试的所有内容。
您可能会问,如果使用Mock ,我们如何确定门和窗是否会真正关闭?答案是我们做不到。但是我们不在乎。这不是SecurityCentral的责任。当门和窗收到正确的信号时,这是门窗自行关闭的责任。我们可以在不同的单元测试中对其进行独立测试。
网友评论