在自动化测试中,我们经常会使用一些简化的对象副本,它们的行为和行为与生产环境相似。引入这样的副本可以降低构建测试用例的复杂度,让我们可以独立、解耦地测试一个模块,而不用担心受到系统其他部分的影响;这种类型的对象也称为TestDouble。其实对于TestDouble的定义和阐述,众说纷纭。GerardMeszaros在本文中介绍了五种不同的Double类型;人们更喜欢用Mock来统一描述不同的TestDoubles。然而,对测试替身实现的误解仍然可能影响测试的设计,使测试用例变得混乱和脆弱,最终导致不必要的重构。本文从作者个人的角度描述了常见的TestDoubles类型及其具体实现:Fake、Stub和Mock,并给出了Double的不同使用场景。假货假货是具有有效实现的对象,但与生产对象不同。通常他们会走一些捷径并简化生产代码版本。如下图所示,Fake可以是DataAccessObject或Repository的基于内存的实现;这个实现并没有真正进行数据库操作,而是使用了一个简单的HashMap来存储数据。这使我们能够在不实际启动数据库或执行耗时的外部请求的情况下测试服务。@Profile("transient")publicclassFakeAccountRepositoryimplementsAccountRepository{Mapaccounts=newHashMap<>();publicFakeAccountRepository(){this.accounts.put(newUser("john@bmail.com"),newUserAccount());this.accounts.put(newUser("boby@bmail.com"),newAdminAccount());}StringgetPasswordHash(Useruser){returnaccounts.get(user).getPasswordHash();}}除了应用于测试,Fake也可用于原型制作或峰值模拟期间;我们可以快速构建系统原型并在内存存储上运行整个系统,从而推迟有关数据库设计的一些决策。另一种常见的使用场景是在测试环境中使用Fake来保证支付总是返回成功的结果。StubStub是一个保存预定义数据并在测试期间使用它来接听电话的对象。当我们不能或不想涉及会用真实数据回答或有不良副作用的对象时,可以使用它。Stub是指那些包含有预定义数据并在测试期间返回给调用者的对象。Stub常用于我们不想返回真实数据或引起其他副作用的场景。Stub的典型应用场景是,当一个对象需要从数据库中取数据时,我们不需要像Fake那样实际与数据库交互或者从内存中取数据,而是直接返回预定义的数据。publicclassGradesService{privatefinalGradebookgradebook;publicGradesService(Gradebookgradebook){this.gradebook=gradebook;}DoubleavageGrades(Studentstudent){returnaverage(gradebook.gradesFor(student));}}我们在写测试用例的时候并没有从Gradebook存储中获取数据,而是获取分数列表需要返回的直接在Stub中定义;我们只需要足够的数据来确保平均计算函数被测试。publicclassGradesServiceTest{privateStudentstudent;privateGradebookgradebook;@BeforepublicvoidsetUp()throwsException{gradebook=mock(Gradebook.class);student=newStudent();}@Testpublicvoidcalculates_grades_average_for_student(){when(gradebook.gradesFor(student)).6(thenReturn,10));//stubbinggradebookdoubleaverageGrades=newGradesService(gradebook).averageGrades(student);assertThat(averageGrades).isEqualTo(8.0);}}CommandQuerySeparation称为Makeaquery。比如avarangeGrades,用来返回学生平均成绩的函数就是一个很典型的例子:DoublegetAverageGrades(Studentstudent);。该函数只返回一个值,没有任何其他副作用。正如我们上面介绍的,我们可以使用Stubs来代替提供实际成绩值的函数,从而简化整个测试用例的编写。但除了Query之外,还有另一类方法称为Command。即当某个函数在执行某些操作的同时改变了系统的状态,但是这类函数往往没有返回值:voidsendReminderEmail(Studentstudent);。这种划分方法的方式也是BertrandMeyer在《ObjectOrientedSoftwareConstruction》一书中介绍的CommandQuery分段法。对于Query类型的方法,我们会优先使用Stub而不是方法的返回值,而对于Command类型的方法的测试,我们需要依赖Mock。MockMock是注册它们接收到的调用的对象。在测试断言中,我们可以在Mocks上验证是否执行了所有预期的操作。预期的呼叫。当我们不想在生产环境中实际调用代码或者在测试中难以验证真实代码的执行效果时,我们就会使用Mock来代替那些真实的对象。一个典型的例子就是邮件发送服务的测试。我们不想每次测试都发送电子邮件。毕竟,我们很难验证电子邮件是否真的发送或收到。我们更关注邮件服务是否按照我们的预期在合适的业务流程中被调用。其概念如下图所示:door=door;}voidsecurityOn(){window.close();door.close();}}在上面的代码中,我们并不是真的要关门来测试securityOn方法,所以我们可以设置合适的模拟对象:publicclassSecurityCentralTest{WindowwindowMock=mock(Window.class);DoordoorMock=mock(Door.class);@Testpublicvoidenabling_security_locks_windows_and_doors(){SecurityCentralsecurityCentral=newSecurityCentral(windowMock,doorMock);securityCentral.securityOn().verify(door(Mock))windowMock).close();}}securityOn方法执行后,window和door的Mock对象已经记录了所有的交互信息,可以让我们验证Window和Door是否真的被调用了。可能有人会疑惑,真实环境中门窗真的是关着的吗?其实我们也不能保证,但这不是我们的重点,也不是SecurityCentral类的目标。门窗能否正常关闭,需要关注Door和Window这两个类。【本文为专栏作家“张子雄”原创文章,如需转载,请通过联系作者】点击此处阅读该作者更多好文