学习笔记1 - Test Smell
这本书找来很久了,一直没读。关于软件测试的好书相当少,对于测试代码的重构及模式的书就更加难得了。虽然我才读了前几章,给我的感受是,这本书确实讲的很全面,并且给很多测试中的东西给出了专业的术语,相信当我读完并吸收完这本书后,会有更多的体会和收获。
第一章是全书概述,直接跳到第二章开始我的笔记。Test Smell,测试的坏味道。和我们通常讲的Code
Smell是一样的,不同的是Test Smell是从测试的角度来分析测试过程中的坏味道。测试的坏味道主要分为三类:
- code smells
- behavior smells
- project smells
这三种坏味道通常不是独立存在的,project smells的根源可能来自code smells和behavior
smells。
Project Smells
出现下面的情况,可以认为出现了Project Smells:
- Production Bugs 产品的Bug数量高居不下
- Daily Integration Build 持续构建总是失败,或者需要花费大量时间去解决一个编译不过的问题
- Buggy Tests 太多有问题的测试案例,相反会影响到项目的效率
- Devolopers Not Writing Tests 开发人员不编写测试案例,导致Production
Bugs增多
- Lost Tests 测试案例不足
Behaivor Smells
比较容易发现,不需要刻意去监控,因为测试案例编译不通过,或者是测试案例失败时,通常就是Behavior
Smell出现的时候:
- Fragile Tests 容易失败的测试案例,稍微一点变化就有可能造成案例失败。通常出现在“录制回放”的测试案例中,这样的案例不仅执行不稳定,维护起来也很麻烦。因为自动生成的代码通常难以理解和维护。
- 造成Fragile Tests原因主要有:
- Interface Sensitivity 这是最常见的,比如,开发修改了函数接口,界面上某个按钮进行了调整,都可能导致案例执行失败。
- Behaivor Sensitivity 被测代码的行为发生了变化,测试案例当然要失败了。(如果不失败的话,都说明测试案例有问题)
- Data Sensitivity 使用数据库的程序,如果数据库中的数据发生变化,可能导致案例失败
- Context Sensitivity 环境的变化,导致案例失败。比如:时间变化,硬件设备,系统环境等等
- Erratic Tests 不稳定的测试案例,有时成功,有时失败:
- Interacting Tests 俗称,前面的案例影响了后面的案例。比如,使用Shared
Fixture时,前一个案例将Shared Fixture的状态修改,导致后面的案例都失败。
- Test Run Wars 多个Test Runners使用同一个Shared Fixture,在同一时间执行时。也是指在非常特殊的情况下,才会出现失败的情况。这种BUG,通常也是到最后才会去修复。
- Unrepeatable Tests 不能保证每次执行都是同样的结果,有时候还需要人工干预一下。
- Frequent Debugging 太频繁的调试说明自动化的单元测试做的不够,或者是单元测试中,一次测试了太多的功能,不够单一。
- Fully Automated Tests 使用TDD的敏捷开发人员每隔几分钟就执行一次测试案例,前提是测试案例一定要自动,不需要手工干预。如果需要手工干预(Manual
Intervention),没有几个人愿意经常执行那些测试案例。
- Slow Tests 案例执行一定要快,如果执行慢,将不能很快将结果反馈给开发。当然,也会使得开发不愿意去执行那些又慢又不稳定的案例。解决的办法也是使用Shared
Fixture。
Code Smells
代码的坏味道,基本上和Martin Fowler的Refactoring中讲的一样。通过看代码,可以发现很多坏味道:
- Obscure Tests 如果你的测试案例令人费解,根本不知道你在测试什么。之后的维护者修改这个测试案例,很可能写错,导致成为另一个Buggy
Tests。
- Conditional Test Logic 测试案例逻辑应该尽可能单一,只测试其中一个分支。如果一个测试案例中太多的逻辑,将会让人搞不清楚。
- Hard-Coded Test Data 测试中的用的数据,使用硬编码,会让人无法理解这些数据,以及这些数据有SUT的联系。
- Test Code Duplication 减少重复的代码,增加测试代码的复用性。一个常见的方法是使用Test
Utility Methods。
- Test Logic in Production 在产品代码中加入测试的一些逻辑是不好的,永远也无法保证这些测试用的代码不会在产品中被执行。
这一章只是对Test Smell进行了一些简要的说明,在该书的第二部分有更加详细的针对Test Smell的介绍。
学习笔记2 - Goal Of Test Automation
或许有人觉得单元测试可有可无,因为觉得需要付出太多的精力,而客户并不需要它。这就涉及到投资回报率的问题,其实所付出的用于测试的投资,往往会收获到更多回报。它让我们减少了Bug的数量,减少了调试代码寻找Bug的时间。看下图,有效的自动化测试投资下,测试和开发付出的努力的时间图:
初期,随着测试的介入,开发付出的投入逐渐减少。后期,随着测试案例的完善和自动化,测试和开发所需要付出的投入都减少到一个很低的水平。阴影部分是节省的开发投入。
同时,如果自动化测试做的不好,在后期,将可能加大测试和开发的负担:
自动化测试的目标
- Tests should help us improve quality.
- Tests should help us understand the SUT.
- Tests should reduce (and not introduce) risk.
- Tests should be easy to run.
- Tests should be easy to write and maintain.
- Tests should require minimal maintenance as the
system evolves around them.
Tests should help us improve quality
- Tests as Specification 测试案例可以验证产品的行为,验证是否“building
the right software”
- Bug Repellent 自动化测试的主要目的不是发现Bug,而是预防或防止Bug的出现。
- Defect Localization 顶层的customer tests只会告诉你案例失败了,unit
tests会告诉你为什么失败了,能定位到具体的问题。如果customer tests失败了而所有的unit
tests却通过的话,说明单元测试案例缺失了。(Missing Unit Test)
Tests should help us understand the SUT
测试案例是非常好的文档,它告诉了你SUT做了些什么。同时,它还让你可以直接调试测试案例,单步跟踪,去了解整个系统是如何运作的。
Tests should reduce (and not introduce) risk
- Tests as Safety Net 测试案例应该成为修改代码安全的保障,让我们在重构旧代码时能够大刀阔斧,而不必担心会破坏什么。
- Do No Harm 测试案例不能对产品造成影响,一个重要的原则是,不要修改SUT。
Tests should be easy to run
- Fully Automated Test 完全自动化
- Self-Checking Test 能够自我检查,说白了就是不需要人工去检查案例执行的结果对不对
- Repeatable Test 需要具备可重复性。鼓励开发人员在每一次按下保存按钮时都执行一次测试案例(我没事就喜欢Ctrl+S)。案例需要具备可重复性,必须具备自我清理能力(self-cleaning),通常,是在Test
Fixture的TearDown中进行环境的清理。
Tests should be easy to write and maintain
- Simple Tests 一个首要的原则是,让测试案例尽量的简单,使得其更容易维护。一次只测一种条件(Verify
One Condition per Test)
- Expressive Tests 测试案例要达到表达清晰的目的,比如,可以使用一些Creation
Mehods和Custom Assertion。有点像BDD。
- Separation of Concerns 测试案例要尽量测试单一,独立的功能,不要依赖过多。往往需要前期就介入参与产品的可测性设计(design
for testability)。
Tests should require minimal maintenance as the system
evolves around them
测试案例需要最小化维护的工作,因此,我们需要编写健壮的测试案例(Robust Test)。
---------------
这章讲的是测试的目标,保持案例简单,稳定,容易维护的声音似乎总是在不停的重复着,是不是有点烦了?也许作者就是希望通过这种不停的唠叨,让我们真正记住、接受,并最终体会和感悟到这些东西给我们带来的好处吧。
学习笔记3 - Philosophy of Test Automation
这一章主要讲自动化测试的原则。前面的章节介绍了很多测试的思想,而思想的东西难免有点虚,这一章就是告诉你,遇到了具体的什么问题时,应该怎么办。作者咨询了很多的开发人员和测试人员,同时也和Martin
Fowler就自动化测试的一些原则问题进行了交流,有些是显而易见的,有些又是让人把握不定。因此,这章主要讨论了以下几个问题:
- Test First or Last?
- Tests or Examples?
- Test-by-Test or Test All-at-Once?
- Outside-In or Inside-Out?
- State or Behavior Verification?
- Fixture Design Upfront or Test-by-Test?
Test First or Last?
是应该先写代码还是先写测试案例?作者认为应该先写测试案例,然后再写代码。这也是测试驱动开发和敏捷测试的一个重要原则。这样做的原因有很多,比如:
- 对一个已完成或旧的代码编写测试案例,比在代码完成前编写测试案例难的多。(面对一个庞大的已完成的系统时,确实会让人无从下手)
- 先写测试案例,可以极大的增强代码的可测性。使得后面编写的代码,天生就具备可测试的能力,因为测试案例已经早于它写好了。
- 先写测试案例,可以对后面的编码起到约束作用,避免编码时添加一些臃肿的、根本就不会用到的函数,使得代码看起来更加精简。
个人感受:
先编写测试案例再写代码,的确有很多好处。但是发现真正这样做的人很少,一方面,对于传统的软件开发公司,要做出一些改变确实有些困难。一方面,先编写测试案例带来的好处并不是立竿见影,很多人尝试了一下就放弃了。因此,需要不断的实践,坚持。(我也要努力了)
Tests or Examples?
这一段,说实话,没看懂在讨论什么问题。只看出在表达一个观点,测试案例相对于文档。同时,还提出一个名词:EDD(example-driven
development),但后又提到EDD的框架,如RSpec,JBehave,让我有点摸不着头脑,据我说知,RSpec,JBehave应该是BDD框架才对。
作者最后的观点:Tests are examples.
Test-by-Test or Test All-at-Once?
到底是应该写一个测试案例,写一段代码呢,还是应该先把测试案例都写好,再写代码?这个问题比较有意思,因为这是一个非常实际的问题。迭代开发(incremental
development)中,有一句话:Test a bit, code a bit, test a bit
more。当然,这样的做法是比较理想的,因为这样能够更加准确的定位到代码的问题。但是,作者提到,一个更好的办法,是先列一个提纲,把测试案例的函数都填好,里面的实现为空。然后,每次填充一个测试案例,写一段代码。
我的观点:
和作者一样。比如,在需要编写一个类前,先假设自己就是代码的调用者,把Test Fixture中的测试案例罗列一下,然后再逐个完成测试案例。每编写完一个测试案例,就把相应的代码实现一下。
Outside-In or Inside-Out?
通常,模块之间会有一些依赖和层次结构,应该从最外层的调用模块开始写案例呢,还是从最里层开始写案例呢?作者的观点是从外到里。
先看下从里到外的情形:
上图,从里到外的开发过程更像传统的开发过程,容易理解,实施起来相对简单。但是,这样的顺序有个缺点,就是上层的SUT必须依赖于已经实现的底层的SUT。如果两个模块是不同的人开发的,上层模块的开发必须等底层模块的开发编写完成才能开始工作。同时,最底层的SUT先实现的话,有可能会过度设计,设计出一些上层模块根本就不使用的一些特性。最终使得整个程序的可测性降低。因此,从外向里的过程会好一些:
先编写最外层的测试案例,可以使用Test Double对象替代被调用的底层模块,使得SUT天然就具备很好的可测性(依赖注入)。同时,由于时刻都是保持“从用户或调用者的角度去思考”,使得SUT对象实现起来目标更加明确,实现的更加精简,从而避免了过度的设计。
State or Behavior Verification?
提出的问题是,应该使用基于状态的验证,还是基于行为的验证?基于状态的验证是指在调用SUT后只检查SUT的状态,比如返回值,比如一个求和函数,最后检查一下求和的结果是否正确。而基于行为的验证,通常是,SUT被调用后,不仅仅改变SUT的状态,还会产生其他影响。比如,一个用户注册的函数,除了要检查返回值是否注册成功,还要坚持数据库中是否写入了新的用户记录。BDD(behavior-driven
development),就是基于行为的验证方式。作者最后说,他主要使用基于状态的验证,但有时为了追求代码覆盖率,会使用基于行为的验证。
我的理解:
对于功能单一,简单,设计良好的代码,使用State Verification确实已经足够。但往往真实的系统是很复杂的,模块之间互相调用,单个函数的功能可能也不是那么单一。基于行为的测试,其实就是站在了用户的角度,去验证各种行为所产生的各种影响。
Fixture Design Upfront or Test-by-Test?
Fixture是某一类案例的集合,一种观点是,让很多案例都共享一个Fixture,每个测试案例的方法执行时都会创建一个新的Fixture实例,并在案例前执行其中的SetUp方法。另一种观点是,前一个观点的做法,会让案例看起来不那么清晰,不容易找到一个测试案例的方法到底会执行哪些SetUp或TearDown的方法。因此,提出了在每个测试案例方法中,使用自定义的Minimal
Fixture,而不是使用一个大的,不容易找到或理解的Fixture。
我的感受:
这一点我也有感受,我也发现我写的一些测试案例,都喜欢让很多Test Class继承一个基类Fixture,在里面定义SetUp和TearnDown,同时,子类中,还可以添加额外方法执行一些准备和清理的操作。这样,使得我的测试案例看起来并不清晰,因为很难从我的测试案例的函数中看出,我到底在SetUp里做了些什么,以及执行了哪些SetUp操作。
学习笔记4 - Principles of Test Automation
自动化测试过程中,有一些基本的原则,就如同宣言(Manifesto)。由于大部分的原则在前面其实都提到的,因此,有的不做太多说明了。
原则:Write the Tests First
原则:Design for Testability
原则:Use the Front Door First
意思是说,从最外层暴露的publish方法开始测试。
原则:Communicate Intent
意思是说,测试案例要意图明确,这样才容易理解和维护。比如,命名时,使用Intent-Revealing
Names[SBPP]。参考:http://architects.dzone.com/news/intention-revealing-designs
原则:Don’t Modify the SUT
不要修改被测的对象。但有时需要使用一些Test Double或Fake对象时,要注意每个替代的对象都被测试。举了个例子,有X,Y,Z三个模块,X依赖于Y和Z,Y依赖于Z,测试X时,可以使用Test
Double代替Y和Z,测试Y时,可以使用Test Double代替Z,但是在测试Z时,不能再把Z替换成Test
Double了,因为Z就是被测对象。
原则:Keep Tests Independent
让测试案例独立,能够单独的执行,不依赖于别的案例。
原则:Isolate the SUT
让SUT独立,不依赖外部的环境。SUT往往会依赖一些外部的其他环境,比如,依赖一个外部的应用程序,依赖一个可用的http服务器等等。这使得测试变得不稳定,或者减慢了执行的速度。去除SUT依赖的方法是使用Test
Double替换掉外部的因素。比如,需要http服务器,可以自己搭一个假的http服务器。
原则:Minimize Test Overlap
最小化重复的测试。我们知道,测试案例的组合是无穷无尽的,我们不可能覆盖所有的组合。但我们可以选择最佳的组合。对被测代码的同一个条件进行重复的测试是没有多少意义的。
原则:Minimize Untestable Code
最小化不可测试的代码,方法就是,重构!
原则:Keep Test Logic Out of Production Code
上一节提到了,不要在产品的代码里添加任何测试的逻辑。比如,像if tesing之类的判断。
原则:Verify One Condition per Test
解释的过程中,有个观点比较有意思。手工测试时,通常会做一些复杂的操作,复杂的条件组合在一起,然后看是否出错。这看起来和自动化测试或单元测试完全相反了,为什么呢?这是因为,人能够在复杂的情况下去判断各种执行结果,并能够去分析其中的问题。而我们的案例其实并没有那么智能,为了让案例能够更加精确的定位问题,我们只能检查每个案例只检查一种情况。
原则:Test Conerns Separtely
Separation of concerns的解释见:http://en.wikipedia.org/wiki/Separation_of_concerns
这里的意思大概是让测试案例做好分层,减少重复的部分,从而更加容易定位问题。
原则:Ensure Commensurate Effort and Responsibility
编写和维护测试案例的时间,不能超过实现产品代码的时间。因此,要平衡好测试与开发的付出。
学习笔记5 - xUnit基础
这几节我看的比较快一些,因为内容之间其实是有联系的,所以合在一起做一个笔记。6-10节主要介绍了什么是Fixture,如何保证一个Fresh
Fixture,如何使用Setup,Tearndown,如何进行验证(Verify),等等。
什么是Fixture?
The test fixture is everything
we need to have in place to exercise the SUT.
从作者的英文解释来看,Fixture确实是一个比较难定义的东西,所以作者用了everything这个词。
什么是Fresh Fixture?
一个测试案例一般都包含以下几个步骤:
- Setup
- Exercise
- Verify
- Teardown
Fresh Fixture是指每个案例执行时,都会生成一个全新的Fixture,好处是不受其他案例的影响。避免了Interacting
Tests(之前有提到的)。
什么是Setup?
Setup是案例的准备阶段,主要有三种实现方式:In-line Fixture Setup, Delegated
Setup, Implicit Setup。推荐使用的是Implicit Setup。
In-line Fixture Setup
直接在测试方法内部做一些具体的Setup操作 :
public void testStatus_initial() {
// In-line setup
Airport departureAirport = new Airport("Calgary", "YYC");
Airport destinationAirport = new Airport("Toronto", "YYZ");
Flight flight = new Flight( flightNumber,
departureAirport,
destinationAirport);
// Exercise SUT and verify outcome
assertEquals(FlightState.PROPOSED, flight.getStatus());
// tearDown:
// Garbage-collected
}
缺点是容易造成很多重复的代码,不易维护。
Delegated Setup
相比In-line Fixture Setup,将里面具体的Setup操作提取出来,作为一个公用的方法,提高了复用性。
public void testGetStatus_inital() {
// Setup
Flight flight = createAnonymousFlight();
// Exercise SUT and verify outcome
assertEquals(FlightState.PROPOSED, flight.getStatus());
// Teardown
// Garbage-collected
}
Implicit Setup
几乎所有的xUnit家族的框架都支持SetUp,比如,使用Google Test中指定的函数名SetUp,NUnit使用[Setup]Attribute。这种方法,不需要我们自己去调用Setup方法,框架会在创建Fresh
Fixture后调用Setup。因此,我们只管实现SetUp方法。
Airport departureAirport;
Airport destinationAirport;
Flight flight;
public void testGetStatus_inital() {
// Implicit setup
// Exercise SUT and verify outcome
assertEquals(FlightState.PROPOSED, flight.getStatus());
}
public void setUp() throws Exception{
super.setUp();
departureAirport = new Airport("Calgary", "YYC");
destinationAirport = new Airport("Toronto", "YYZ");
BigDecimal flightNumber = new BigDecimal("999");
flight = new Flight( flightNumber , departureAirport,
destinationAirport);
}
什么是Teardown?
为了保证每个案例都拥有一个Fresh Fixture,必须在案例的结束时做一些清理操作,这就是Teardown。和Setup一样,Teardown也有三种实现方式:In-line
Fixture Teardown, Delegated Teardown, Implicit Teardown。同样,推荐使用Implicit
Teardown。
什么是Shared Fixture?
多个测试方法共用一个Fixture,这时,Setup只会在第一个测试方法执行时被执行。gtest中,同时还拥有一个公共的TearDownTestCases方法。
Result Verification
前面说过,测试案例必须拥有Self-Checking的能力。Verification分两种:State
Verification和Behavior Verification。
State Verification
执行SUT后,验证SUT的状态:
验证时,可以使用Build-in Assertions,比如xUnit框架提供的assertTrue,
assertEquals等方法。或者Custom Assertion等等。
Behavior Verification
不仅仅验证SUT的状态,同时还对SUT的行为对外部因素造成的影响进行验证。
比如下面这个例子:
public void testRemoveFlightLogging_recordingTestStub()
throws Exception {
// fixture setup
FlightDto expectedFlightDto = createAnUnregFlight();
FlightManagementFacade facade =
new FlightManagementFacadeImpl();
// Test Double setup
AuditLogSpy logSpy = new AuditLogSpy();
facade.setAuditLog(logSpy);
// exercise
facade.removeFlight(expectedFlightDto.getFlightNumber());
// verify
assertEquals("number of calls", 1,
logSpy.getNumberOfCalls());
assertEquals("action code",
Helper.REMOVE_FLIGHT_ACTION_CODE,
logSpy.getActionCode());
assertEquals("date", helper.getTodaysDateWithoutTime(),
logSpy.getDate());
assertEquals("user", Helper.TEST_USER_NAME,
logSpy.getUser());
assertEquals("detail",
expectedFlightDto.getFlightNumber(),
logSpy.getDetail());
}
除此之外,我们还可以使用一些Mock框架,使用基于行为的验证方式,这种方式,不需要我们显式的调用验证的方法。(Expected
Behaivor Specification)
public void testRemoveFlight_JMock() throws Exception {
// fixture setup
FlightDto expectedFlightDto = createAnonRegFlight();
FlightManagementFacade facade =
new FlightManagementFacadeImpl();
// mock configuration
Mock mockLog = mock(AuditLog.class);
mockLog.expects(once()).method("logMessage")
.with(eq(helper.getTodaysDateWithoutTime()),
eq(Helper.TEST_USER_NAME),
eq(Helper.REMOVE_FLIGHT_ACTION_CODE),
eq(expectedFlightDto.getFlightNumber()));
// mock installation
facade.setAuditLog((AuditLog) mockLog.proxy());
// exercise
facade.removeFlight(expectedFlightDto.getFlightNumber());
// verify
// verify() method called automatically by JMock
}
如何使测试代码变得简洁,减少重复?
Expected Object
需要比较对象内部很多属性时,使用对象比较会更简单。
原有案例代码:
public void testInvoice_addLineItem7() {
LineItem expItem = new LineItem(inv, product, QUANTITY);
// Exercise
inv.addItemQuantity(product, QUANTITY);
// Verify
List lineItems = inv.getLineItems();
LineItem actual = (LineItem)lineItems.get(0);
assertEquals(expItem.getInv(), actual.getInv());
assertEquals(expItem.getProd(), actual.getProd());
assertEquals(expItem.getQuantity(), actual.getQuantity());
}
改进后:
public void testInvoice_addLineItem8() {
LineItem expItem = new LineItem(inv, product, QUANTITY);
// Exercise
inv.addItemQuantity(product, QUANTITY);
// Verify
List lineItems = inv.getLineItems();
LineItem actual = (LineItem)lineItems.get(0);
assertEquals("Item", expItem, actual);
}
Custom Assersions
需要验证的细节很多时,可以自己定义一个Assersion,隐藏掉这些细节。比如:
static void assertLineItemsEqual(
String msg, LineItem exp, LineItem act) {
assertEquals (msg+" Inv", exp.getInv(), act.getInv());
assertEquals (msg+" Prod", exp.getProd(), act.getProd());
assertEquals (msg+" Quan", exp.getQuantity(), act.getQuantity());
}
Verification Methods
和Custom Asserions很像,唯一不同的是,Custom Assertion只包含验证的代码,Verification
Methods同时还包含对SUT的操作。比如:
void assertInvoiceContainsOnlyThisLineItem(
Invoice inv,
LineItem expItem) {
List lineItems = inv.getLineItems();
assertEquals("number of items", lineItems.size(), 1);
LineItem actual = (LineItem)lineItems.get(0);
assertLineItemsEqual("",expItem, actual);
}
Parameterized and Data-Driven Tests
对于测试逻辑一致,只是测试数据有不同的测试案例,适合使用参数化测试,或者叫数据驱动测试。比如,Google
Test就很好的提供了参数化的测试,见:
玩转 Google开源C++单元测试框架Google Test系列(gtest)之四 - 参数化
通过参数化,可以简化测试代码,不需要为大量不同的输入数据分别编写测试案例。
Avoiding Conditional Test Logic
验证时,不要使用一些条件相关的逻辑!比如,不要使用if,
loop之类的语句!下面是一个例子:
使用if的情况:
List lineItems = invoice.getLineItems();
if (lineItems.size() == 1) {
LineItem expected =
new LineItem(invoice, product,5,
new BigDecimal("30"),
new BigDecimal("69.96"));
LineItem actItem = (LineItem) lineItems.get(0);
assertEquals("invoice", expected, actItem);
} else {
fail("Invoice should have exactly one line item");
}
可以看出,上面的写法是不好的,验证中有逻辑判断意味着有可能案例不够单一,使得案例难以理解。因此,比较好的是改成下面的方式:
List lineItems = invoice.getLineItems();
assertEquals("number of items", lineItems.size(), 1);
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.96"));
LineItem actItem = (LineItem) lineItems.get(0);
assertEquals("invoice", expected, actItem);
Working Backward
一个编写测试案例的小技巧或者说是习惯吧,就是实现一个测试案例时,从最后一行开始写起,比如,先写Assertions。可以一试。
Using Test-Driven Development to Write Test Utility
Method
我们经常实现一些测试用的辅助方法,这些方法在实现过程中,使用TDD的方式去实现,编写一些简单的测试案例,保证辅助方法也是正确的。也就是说,测试案例代码本身也是需要被测试的。
学习笔记6 - Test Double
我不知道Test Double翻译成中文是什么,测试替身?Test Double就像是陈龙大哥电影里的替身,起到以假乱真的作用。在单元测试时,使用Test
Double减少对被测对象的依赖,使得测试更加单一,同时,让测试案例执行的时间更短,运行更加稳定,同时能对SUT内部的输入输出进行验证,让测试更加彻底深入。但是,Test
Double也不是万能的,Test Double不能被过度使用,因为实际交付的产品是使用实际对象的,过度使用Test
Double会让测试变得越来越脱离实际。
我感觉,Test Double这玩意比较适合在Java,C#等完全面向对象的语言中使用。并且需要很好的使用依赖注入(Dependency
injection)设计。如果被测系统是使用C或C++开发,使用Test Double将是一个非常困难和痛苦的事情。
要理解Test Double,必须非常清楚以下几个东西的关系,本文的重点也是说明一下他们之间的关系。他们分别是:
- Dummy Object
- Test Stub
- Test Spy
- Mock Object
- Fake Object
Dummy Object
Dummy Objects泛指在测试中必须传入的对象,而传入的这些对象实际上并不会产出任何作用,仅仅是为了能够调用被测对象而必须传入的一个东西。
使用Dummy Object的例子:
public void testInvoice_addLineItem_DO() {
final int QUANTITY = 1;
Product product = new Product("Dummy Product Name",
getUniqueNumber());
Invoice inv = new Invoice( new DummyCustomer() );
LineItem expItem = new LineItem(inv, product, QUANTITY);
// Exercise
inv.addItemQuantity(product, QUANTITY);
// Verify
List lineItems = inv.getLineItems();
assertEquals("number of items", lineItems.size(), 1);
LineItem actual = (LineItem)lineItems.get(0);
assertLineItemsEqual("", expItem, actual);
}
Test Stub
测试桩是用来接受SUT内部的间接输入(indirect inputs),并返回特定的值给SUT。可以理解Test
Stub是在SUT内部打的一个桩,可以按照我们的要求返回特定的内容给SUT,Test Stub的交互完全在SUT内部,因此,它不会返回内容给测试案例,也不会对SUT内部的输入进行验证。
使用Test Stub的例子:
public void testDisplayCurrentTime_exception()
throws Exception {
// Fixture setup
Testing with Doubles 136 Chapter 11 Using Test Doubles
// Define and instantiate Test Stub
TimeProvider testStub = new TimeProvider()
{ // Anonymous inner Test Stub
public Calendar getTime() throws TimeProviderEx {
throw new TimeProviderEx("Sample");
}
};
// Instantiate SUT
TimeDisplay sut = new TimeDisplay();
sut.setTimeProvider(testStub);
// Exercise SUT
String result = sut.getCurrentTimeAsHtmlFragment();
// Verify direct output
String expectedTimeString =
"<span class=\"error\">Invalid Time</span>";
assertEquals("Exception", expectedTimeString, result);
}
Test Spy
Test Spy像一个间谍,安插在了SUT内部,专门负责将SUT内部的间接输出(indirect outputs)传到外部。它的特点是将内部的间接输出返回给测试案例,由测试案例进行验证,Test
Spy只负责获取内部情报,并把情报发出去,不负责验证情报的正确性。
使用Test Spy的例子:
public void testRemoveFlightLogging_recordingTestStub()
throws Exception {
// Fixture setup
FlightDto expectedFlightDto = createAnUnregFlight();
FlightManagementFacade facade =
new FlightManagementFacadeImpl();
// Test Double setup
AuditLogSpy logSpy = new AuditLogSpy();
facade.setAuditLog(logSpy);
// Exercise
facade.removeFlight(expectedFlightDto.getFlightNumber());
// Verify state
assertFalse("flight still exists after being removed",
facade.flightExists( expectedFlightDto.
getFlightNumber()));
// Verify indirect outputs using retrieval interface of spy
assertEquals("number of calls", 1,
logSpy.getNumberOfCalls());
assertEquals("action code",
Helper.REMOVE_FLIGHT_ACTION_CODE,
logSpy.getActionCode());
assertEquals("date", helper.getTodaysDateWithoutTime(),
logSpy.getDate());
assertEquals("user", Helper.TEST_USER_NAME,
logSpy.getUser());
assertEquals("detail",
expectedFlightDto.getFlightNumber(),
logSpy.getDetail());
}
Mock Object
Mock Object和Test Spy有类似的地方,它也是安插在SUT内部,获取到SUT内部的间接输出(indirect
outputs),不同的是,Mock Object还负责对情报(indirect
outputs)进行验证,总部(外部的测试案例)信任Mock Object的验证结果。
Mock的测试框架有很多,比如:NMock,JMock等等。如果使用Mock Object,建议使用现成的Mock框架,因为框架为我们做了很多琐碎的事情,我们只需要对Mock对象进行一些描述。比如,通常Mock框架都会使用基于行为(Behavior)的描述性调用方法,即,在调用SUT前,只需要描述Mock对象预期会接收什么参数,会执行什么操作,返回什么内容等,这样的案例更加具有可读性。比如下面使用Mock的测试案例:
public void testRemoveFlight_Mock() throws Exception {
// Fixture setup
FlightDto expectedFlightDto = createAnonRegFlight();
// Mock configuration
ConfigurableMockAuditLog mockLog =
new ConfigurableMockAuditLog();
mockLog.setExpectedLogMessage(
helper.getTodaysDateWithoutTime(),
Helper.TEST_USER_NAME,
Helper.REMOVE_FLIGHT_ACTION_CODE,
expectedFlightDto.getFlightNumber());
mockLog.setExpectedNumberCalls(1);
// Mock installation
FlightManagementFacade facade =
new FlightManagementFacadeImpl();
facade.setAuditLog(mockLog);
// Exercise
facade.removeFlight(expectedFlightDto.getFlightNumber());
// Verify
assertFalse("flight still exists after being removed",
facade.flightExists( expectedFlightDto.
getFlightNumber()));
mockLog.verify();
}
Fake Object
经常,我们会把Fake Object和Test Stub搞混,因为它们都和外部没有交互,对内部的输入输出也不进行验证。不同的是,Fake
Object并不关注SUT内部的间接输入(indirect inputs)或间接输出(indirect
outputs),它仅仅是用来替代一个实际的对象,并且拥有几乎和实际对象一样的功能,保证SUT能够正常工作。实际对象过分依赖外部环境,Fake
Object可以减少这样的依赖。需要使用Fake Object通常符合以下情形:
- 实际对象还未实现出来,先用一个简单的Fake Object代替它。
- 实际对象执行需要太长的时间
- 实际对象在实际环境下可能会有不稳定的情况。比如,网络发送数据包,不能保证每次都能成功发送。
- 实际对象在实际系统环境下不可用,或者很难让它变得可用。比如,使用一个依赖实际数据库的数据库访问层对象,必须安装数据库,并且进行大量的配置,才能生效。
一个使用Fake Object的例子是,将一个依赖实际数据库的数据库访问层对象替换成一个基于内存,使用Hash
Table对数据进行管理的数据访问层对象,它具有和实际数据库访问层一样的接口实现。
public class InMemoryDatabase implements FlightDao{
private List airports = new Vector();
public Airport createAirport(String airportCode,
String name, String nearbyCity)
throws DataException, InvalidArgumentException {
assertParamtersAreValid( airportCode, name, nearbyCity);
assertAirportDoesntExist( airportCode);
Airport result = new Airport(getNextAirportId(),
airportCode, name, createCity(nearbyCity));
airports.add(result);
return result;
}
public Airport getAirportByPrimaryKey(BigDecimal airportId)
throws DataException, InvalidArgumentException {
assertAirportNotNull(airportId);
Airport result = null;
Iterator i = airports.iterator();
while (i.hasNext()) {
Airport airport = (Airport) i.next();
if (airport.getId().equals(airportId)) {
return airport;
}
}
throw new DataException("Airport not found:"+airportId);
}
说了这么多,可能更加糊涂了。在实际使用时,并不需要过分在意使用的是哪种Test Double。当然,作为思考,可以想一想,以前测试过程中做的一些所谓的“假的”东西,到底是Dummy
Object, Test Stub, Test Spy, Mock Object, 还是Fake Object呢?
学习笔记7(完) - 总结
总体来说,这本书是不错的。后面的第二部分和第三部分,主要针对Test Smells和Test Patterns进行了更加具体的描述,主要的内容和第一部分还是基本一致的。因此,我主要的精力都花在了第一部分,对于后面的第二部分和第三部分,我是跳着看完的。后面的内容当作字典来查询会比较好,因为写的比较详细。
其中有一个观点我觉得是比较新颖的,如果你分不清现在做的测试是不是属于单元测试,就请参照一下下面的吧:
A test is not a unit test if:
- It talks to the database.
- It communicates across the network.
- It touches the file system.
- It can’t run correctly at the same time as any of
your other unit tests.
- You have to do special things to your environment
(such as editing config files) to run
it.
在讲到代码的可测性设计时,作者提出主要从以下几个方面来提高可测性:
Dependency Injection
依赖注入,代码设计中并不依赖于具体的实现,并且运行将特定的实现注入到系统中。注入主要有三种方式:
- Parameter Injection
- Constructor Injection
- Setter Injection
Dependency Lookup
依赖查找,运行时动态查找到所依赖的具体对象。
Humble Object
这个名称很抽象,我的理解是在代码中提供一些接口出来,方面测试。
Test Hook
在代码中加入一些测试逻辑,比如,如果是测试模块,就怎么怎么样。不过,我想这种方式应该是不值得推荐的。
最后,很欣慰在春节前把这本书看完了,可以说收获还是蛮大的。以后遇到什么问题,可以拿这本书当资料来查。xUnit
Test Patterns的官方地址如下:
http://xunitpatterns.com/
可以在这个地址查询到每个测试模式和相关的名称解释。
作者:CoderZh(CoderZh的技术博客
- 博客园)
出处:http://coderzh.cnblogs.com/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 |