自动化测试中,不得不知的替身对象

| 2021-02-13

一个测试替身,是一种可以在一个测试用例中用来代替真实对象的对象,类似于特技替身代替电影中的真实演员。这些测试有时通常被称为“「Mock」(模拟)”,但区分不同类型的测试替身很重要,因为它们都有不同的用途。最常见的测试替身类型是「stub(桩)」、「mock」和「Fake」。

一个 Stub 自己没有逻辑,它只返回你让它返回的内容。当您需要一个对象返回特定的值,以便将测试用例中的代码置于特定状态时,可以使用「Stub」。尽管通过手工编写的方式也很方便,但使用一个 Mock框架 常常是一个更方便地减少很多模板代码。

// Pass in a stub that was created by a mocking framework.
AccessManager accessManager = new AccessManager(stubAuthenticationService);
// The user shouldn't have access when the authentication service returns false.
when(stubAuthenticationService.isAuthenticated(USER_ID)).thenReturn(false);
assertFalse(accessManager.userHasAccess(USER_ID));
// The user should have access when the authentication service returns true.
when(stubAuthenticationService.isAuthenticated(USER_ID)).thenReturn(true);
assertTrue(accessManager.userHasAccess(USER_ID));

一个 Mock 是用来验证预期的调用是否真的发生了。如果它没有被调用,测试就应该失败。Mock 用于测试对象之间的交互,通常在被测系统(SUT)没有其它可见的状态变更或没有返回可验证结果的的情况下,Mock 非常有用(例如,当代码从磁盘读取某些内容,并且希望确保它不会执行多个磁盘读取,则可以使用 Mock 来验证,它只调用了一次执行读取的方法)。

// Pass in a mock that was created by a mocking framework.
AccessManager accessManager = new AccessManager(mockAuthenticationService);
accessManager.userHasAccess(USER_ID);
// The test should fail if accessManager.userHasAccess(USER_ID) didn't call
// mockAuthenticationService.isAuthenticated(USER_ID) or if it called it more than once.
verify(mockAuthenticationService).isAuthenticated(USER_ID);

一个 Fake (赝品)是不使用模拟框架的,而是真实API的一个轻量级实现,其行为与真实的代码实现很类似,但不适合于生产环境(例如,内存数据库就是生产数据库的赝品)。当你不能在测试中使用真正的实现时(例如当真实的实现速度太慢或需要通过网络进行通信时),就可以使用 Fake。您常常不需要自己编写这些 Fake ,因为它(Fake)通常应该由负责那个真正实现代码的人员或团队来创建和维护。

// Creating the fake is fast and easy.
AuthenticationService fakeAuthenticationService = new FakeAuthenticationService();
AccessManager accessManager = new AccessManager(fakeAuthenticationService);
// The user shouldn't have access since the authentication service doesn't
// know about the user.
assertFalse(accessManager.userHasAccess(USER_ID));
// The user should have access after it's added to the authentication service.
fakeAuthenticationService.addAuthenticatedUser(USER_ID);
assertTrue(accessManager.userHasAccess(USER_ID));

“测试替身(test double)”一词是杰拉德·梅萨罗斯(Gerard Meszaros)在《xUnit测试模式(xUnit Test Patterns)》一书中创造的,你可以在这本书的网站中看到对此类对象的定义。他对这类对象统一称呼为:Test Double。包含:**Dummy,Fake,Spy,Mock 和 Stub。**你也可以在 Martin Fowler 的文章 中看到一些相关的讨论。

测试替身家族

对于 Test Doubles 实现的误解很可能会影响到测试的设计,使测试用例变得混乱和脆弱,最终带来不必要的重构。下面我们来了解一下它们之间的些许不同之处,以及各自的使用场景。

哑对象 (Dummy Object)

Dummy是在测试中仅发挥占位填充符的作用,在测试中不会被使用,也不参与测试行为或状态的验证。常见的场景是作为被测函数的某个参数占位符,以减少参数的构造成本。

冒充对象(Fake Object)

Fake 是模拟被测系统所依赖的那个组件,它是生产环境下这个被依赖组件的功能实现的简化版本。Fake对象是用于测试的,但它既不是测试中的控制点(control point ),也不是观测点(observation point )。

一个常见的使用场景就是利用 Fake 来保证在测试环境下支付永远返回成功结果,前提是被测对象并不是支付系统。

桩对象(Stub Object)

Stub提供在测试过程中对请求调用的屏蔽式应答,通常对该测试程序之外的任何内容都无响应。即:Stub只是返回一个规定的值,而不会去涉及到系统的任何改变。它通常是测试中的控制点(control point)。

比较常见的场景就是系统希望去查询某一类的信息,而Stub可以总是返回一个固定值,比如发送邮件的功能,Stub可以总是返回邮件发送成功的标识1,但是你并不知道你到底发送了邮件给谁或者发送了几封邮件。

间谍对象(Spy Object)

Spy可以看做是一类Stub,但是,它会记录它在被调用后的一些信息。例如,它被用于模拟email 服务,并记录用它发送了多少邮件。

交互模拟对象(Mock Object)

Mock 通常会被作为观察点,用于验证被测系统(SUT)在执行时的间接输出(即观测点)。通常,Mock对象还会发挥Stub的作用,因为如果测试尚未失败,它必须将值返回到SUT,但其重点是验证间接输出。因此,一个Mock对象不仅仅是一个Stub和断言,它的使用方式有着根本的不同。所以,它既可以是观察点( Observation Point ),也可以发挥 控制点( Control Point )的作用。

MockStub有一定的重合性,比较大的区别是 Mock 专注于观察点,而 Stub 专注于 控制点。或者从另一个角度上面来说,Mock会验证行为的变更,而Stub只是状态的一个变化而已


发表时间:July 18, 2013

原文作者: Andrew Trenk

原文链接Testing on the Toilet: Know Your Test Doubles