测试中 Fakes、Mocks 以及 Stubs 概念明晰

开发 开发工具
在 securityOn 方法执行之后,window 与 door 的 Mock 对象已经记录了所有的交互信息,这就允许我们能够去验证 Window 与 Door 是否被真实的调用。

自动化测试中,我们常会使用一些经过简化的,行为与表现类似于生产环境下的对象的复制品。引入这样的复制品能够降低构建测试用例的复杂度,允许我们独立而解耦地测试某个模块,不再担心受到系统中其他部分的影响;这类型对象也就是所谓的 Test Double。实际上对于 Test Double 的定义与阐述也是见仁见智,Gerard Meszaros 在这篇文章中就介绍了五个不同的 Double 类型;而人们更倾向于使用 Mock 来统一描述不同的 Test Doubles。不过对于 Test Doubles 实现的误解还是可能会影响到测试的设计,使测试用例变得混乱和脆弱,最终带来不必要的重构。本文则是从作者个人的角度描述了常见的 Test Doubles 类型及其具体的实现:Fake、Stub 与 Mock,并且给出了不同的 Double 的使用场景。

[[191910]]

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 是那些包含了生产环境下具体实现的简化版本的对象。

如下图所示,Fake 可以是某个 Data Access Object 或者 Repository 的基于内存的实现;该实现并不会真的去进行数据库操作,而是使用简单的 HashMap 来存放数据。这就允许了我们能够在并没有真的启动数据库或者执行耗时的外部请求的情况下进行服务的测试。

  1. @Profile("transient"
  2. public class FakeAccountRepository implements AccountRepository { 
  3.  
  4.    Map<User, Account> accounts = new HashMap<>(); 
  5.  
  6.    public FakeAccountRepository() { 
  7.        this.accounts.put(new User("john@bmail.com"), new UserAccount()); 
  8.        this.accounts.put(new User("boby@bmail.com"), new AdminAccount()); 
  9.    } 
  10.  
  11.    String getPasswordHash(User user) { 
  12.        return accounts.get(user).getPasswordHash(); 
  13.    } 

除了应用到测试,Fake 还能够用于进行原型设计或者峰值模拟中;我们能够迅速地实现系统原型,并且基于内存存储来运行整个系统,推迟有关数据库设计所用到的一些决定。另一个常见的使用场景就是利用 Fake 来保证在测试环境下支付永远返回成功结果。

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 常被用于我们不希望返回真实数据或者造成其他副作用的场景。

Stub 的典型应用场景即是当某个对象需要从数据库抓取数据时,我们并不需要真实地与数据库进行交互或者像 Fake 那样从内存中抓取数据,而是直接返回预定义好的数据。

  1. public class GradesService { 
  2.  
  3.    private final Gradebook gradebook; 
  4.  
  5.    public GradesService(Gradebook gradebook) { 
  6.        this.gradebook = gradebook; 
  7.    } 
  8.  
  9.    Double averageGrades(Student student) { 
  10.        return average(gradebook.gradesFor(student)); 
  11.    } 

我们在编写测试用例时并没有从 Gradebook 存储中抓取数据,而是在 Stub 中直接定义好需要返回的成绩列表;我们只需要足够的数据来保证对平均值计算函数进行测试就好了。

  1. public class GradesServiceTest { 
  2.  
  3.    private Student student; 
  4.    private Gradebook gradebook; 
  5.  
  6.    @Before 
  7.    public void setUp() throws Exception { 
  8.        gradebook = mock(Gradebook.class); 
  9.        student = new Student(); 
  10.    } 
  11.  
  12.    @Test 
  13.    public void calculates_grades_average_for_student() { 
  14.        when(gradebook.gradesFor(student)).thenReturn(grades(8, 6, 10)); //stubbing gradebook 
  15.  
  16.        double averageGrades = new GradesService(gradebook).averageGrades(student); 
  17.  
  18.        assertThat(averageGrades).isEqualTo(8.0); 
  19.    } 

Command Query Separation

仅返回部分结果而并没有真实改变系统状态的的方法被称作查询(Query)。譬如 avarangeGrades,用于返回学生成绩平均值的函数就是非常典型的例子:Double getAverageGrades(Student student);。该函数仅返回了某个值,而没有其他的任何副作用。正如我们上文中介绍的,我们可以使用 Stubs 来替换提供实际成绩值的函数,从而简化了整个测试用例的编写。不过除了 Query 之外还有另一个类别的方法,被称作 Command。即当某个函数在执行某些操作的时候还改变了系统状态,不过该类型函数往往没有什么返回值:void sendReminderEmail(Student student);。这种对于方法的划分方式也就是 Bertrand Meyer 在Object Oriented Software Construction 一书中介绍的 Command Query 分割法。

对于 Query 类型的方法我们会优先考虑使用 Stub 来代替方法的返回值,而对于 Command 类型的方法的测试则需要依赖于 Mock。

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 被进行了符合期望的调用。

当我们并不希望真的调用生产环境下的代码或者在测试中难于验证真实代码执行效果的时候,我们会用 Mock 来替代那些真实的对象。典型的例子即是对邮件发送服务的测试,我们并不希望每次进行测试的时候都发送一封邮件,毕竟我们很难去验证邮件是否真的被发出了或者被接收了。我们更多地关注于邮件服务是否按照我们的预期在合适的业务流中被调用,其概念如下图所示: 

  1. public class SecurityCentral { 
  2.  
  3.    private final Window window; 
  4.    private final Door door; 
  5.  
  6.    public SecurityCentral(Window window, Door door) { 
  7.        this.window = window; 
  8.        this.door = door; 
  9.    } 
  10.  
  11.    void securityOn() { 
  12.        window.close(); 
  13.        door.close(); 
  14.    } 

在上述代码中,我们并不想真的去关门来测试 securityOn 方法,因此我们可以设置合适的 Mock 对象:

  1. public class SecurityCentralTest { 
  2.  
  3.    Window windowMock = mock(Window.class); 
  4.    Door doorMock = mock(Door.class); 
  5.  
  6.    @Test 
  7.    public void enabling_security_locks_windows_and_doors() { 
  8.        SecurityCentral securityCentral = new SecurityCentral(windowMock, doorMock); 
  9.  
  10.        securityCentral.securityOn(); 
  11.  
  12.        verify(doorMock).close(); 
  13.        verify(windowMock).close(); 
  14.    } 

在 securityOn 方法执行之后,window 与 door 的 Mock 对象已经记录了所有的交互信息,这就允许我们能够去验证 Window 与 Door 是否被真实的调用。或许有人会疑问是否在真实环境下门与窗是否被真的关闭了?其实我们并不能保证,不过这也不是我们关注的点,也不是 SecurityCentral 这个类关注的目标。门与窗是否能被正常的关闭应该是由 Door 与 Window 这两个类所关注的。

【本文是51CTO专栏作者“张梓雄 ”的原创文章,如需转载请通过51CTO与作者联系】

戳这里,看该作者更多好文

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2009-07-09 17:09:49

MyEclipse

2010-01-19 17:23:11

TongWeb

2009-08-28 13:12:56

C#反射实例C#反射

2019-04-17 15:35:37

Redis数据库数据结构

2012-05-29 09:42:08

Linux服务器窗口管理

2009-09-09 10:47:29

C# CheckBox

2010-08-14 21:59:35

2020-12-08 12:24:55

接口测试Interface

2009-10-12 17:02:13

2010-09-14 10:16:55

服务器虚拟化

2017-04-28 14:25:06

支付卡合规方案

2009-09-04 17:53:51

C# Main函数

2009-12-25 15:36:29

双线路接入技术

2021-08-06 06:38:49

安卓应用Android 性能测试

2013-07-29 10:27:19

2009-09-11 03:21:00

网络故障诊断

2011-05-20 17:59:06

回调函数

2012-03-12 09:39:38

大数据IT资源

2012-02-07 09:01:15

亚马逊EC2云服务

2009-11-24 10:06:21

SUSE enterp
点赞
收藏

51CTO技术栈公众号