求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
 
《xUnit Test Patterns》学习笔记系列
 

2010-08-16 作者:CoderZh 来源:CoderZh的技术博客

 

学习笔记1 - Test Smell

这本书找来很久了,一直没读。关于软件测试的好书相当少,对于测试代码的重构及模式的书就更加难得了。虽然我才读了前几章,给我的感受是,这本书确实讲的很全面,并且给很多测试中的东西给出了专业的术语,相信当我读完并吸收完这本书后,会有更多的体会和收获。

第一章是全书概述,直接跳到第二章开始我的笔记。Test Smell,测试的坏味道。和我们通常讲的Code Smell是一样的,不同的是Test Smell是从测试的角度来分析测试过程中的坏味道。测试的坏味道主要分为三类:

  1. code smells
  2. behavior smells
  3. project smells

这三种坏味道通常不是独立存在的,project smells的根源可能来自code smells和behavior smells。

Project Smells

出现下面的情况,可以认为出现了Project Smells:

  1. Production Bugs 产品的Bug数量高居不下
  2. Daily Integration Build 持续构建总是失败,或者需要花费大量时间去解决一个编译不过的问题
  3. Buggy Tests 太多有问题的测试案例,相反会影响到项目的效率
  4. Devolopers Not Writing Tests 开发人员不编写测试案例,导致Production Bugs增多
  5. Lost Tests 测试案例不足

Behaivor Smells

比较容易发现,不需要刻意去监控,因为测试案例编译不通过,或者是测试案例失败时,通常就是Behavior Smell出现的时候:

  1. Fragile Tests 容易失败的测试案例,稍微一点变化就有可能造成案例失败。通常出现在“录制回放”的测试案例中,这样的案例不仅执行不稳定,维护起来也很麻烦。因为自动生成的代码通常难以理解和维护。
  2. 造成Fragile Tests原因主要有:
    • Interface Sensitivity 这是最常见的,比如,开发修改了函数接口,界面上某个按钮进行了调整,都可能导致案例执行失败。
    • Behaivor Sensitivity 被测代码的行为发生了变化,测试案例当然要失败了。(如果不失败的话,都说明测试案例有问题)
    • Data Sensitivity 使用数据库的程序,如果数据库中的数据发生变化,可能导致案例失败
    • Context Sensitivity 环境的变化,导致案例失败。比如:时间变化,硬件设备,系统环境等等
  3. Erratic Tests 不稳定的测试案例,有时成功,有时失败:
    • Interacting Tests 俗称,前面的案例影响了后面的案例。比如,使用Shared Fixture时,前一个案例将Shared Fixture的状态修改,导致后面的案例都失败。
    • Test Run Wars 多个Test Runners使用同一个Shared Fixture,在同一时间执行时。也是指在非常特殊的情况下,才会出现失败的情况。这种BUG,通常也是到最后才会去修复。
    • Unrepeatable Tests 不能保证每次执行都是同样的结果,有时候还需要人工干预一下。
  4. Frequent Debugging 太频繁的调试说明自动化的单元测试做的不够,或者是单元测试中,一次测试了太多的功能,不够单一。
  5. Fully Automated Tests 使用TDD的敏捷开发人员每隔几分钟就执行一次测试案例,前提是测试案例一定要自动,不需要手工干预。如果需要手工干预(Manual Intervention),没有几个人愿意经常执行那些测试案例。
  6. Slow Tests 案例执行一定要快,如果执行慢,将不能很快将结果反馈给开发。当然,也会使得开发不愿意去执行那些又慢又不稳定的案例。解决的办法也是使用Shared Fixture。

Code Smells

代码的坏味道,基本上和Martin Fowler的Refactoring中讲的一样。通过看代码,可以发现很多坏味道:

  1. Obscure Tests 如果你的测试案例令人费解,根本不知道你在测试什么。之后的维护者修改这个测试案例,很可能写错,导致成为另一个Buggy Tests。
  2. Conditional Test Logic 测试案例逻辑应该尽可能单一,只测试其中一个分支。如果一个测试案例中太多的逻辑,将会让人搞不清楚。
  3. Hard-Coded Test Data 测试中的用的数据,使用硬编码,会让人无法理解这些数据,以及这些数据有SUT的联系。
  4. Test Code Duplication 减少重复的代码,增加测试代码的复用性。一个常见的方法是使用Test Utility Methods。
  5. Test Logic in Production 在产品代码中加入测试的一些逻辑是不好的,永远也无法保证这些测试用的代码不会在产品中被执行。

这一章只是对Test Smell进行了一些简要的说明,在该书的第二部分有更加详细的针对Test Smell的介绍。

学习笔记2 - Goal Of Test Automation

或许有人觉得单元测试可有可无,因为觉得需要付出太多的精力,而客户并不需要它。这就涉及到投资回报率的问题,其实所付出的用于测试的投资,往往会收获到更多回报。它让我们减少了Bug的数量,减少了调试代码寻找Bug的时间。看下图,有效的自动化测试投资下,测试和开发付出的努力的时间图:

初期,随着测试的介入,开发付出的投入逐渐减少。后期,随着测试案例的完善和自动化,测试和开发所需要付出的投入都减少到一个很低的水平。阴影部分是节省的开发投入。

同时,如果自动化测试做的不好,在后期,将可能加大测试和开发的负担:

自动化测试的目标

  1. Tests should help us improve quality.
  2. Tests should help us understand the SUT.
  3. Tests should reduce (and not introduce) risk.
  4. Tests should be easy to run.
  5. Tests should be easy to write and maintain.
  6. Tests should require minimal maintenance as the system evolves around them.

Tests should help us improve quality

  1. Tests as Specification 测试案例可以验证产品的行为,验证是否“building the right software”
  2. Bug Repellent 自动化测试的主要目的不是发现Bug,而是预防或防止Bug的出现。
  3. 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

  1. Tests as Safety Net 测试案例应该成为修改代码安全的保障,让我们在重构旧代码时能够大刀阔斧,而不必担心会破坏什么。
  2. Do No Harm 测试案例不能对产品造成影响,一个重要的原则是,不要修改SUT。

Tests should be easy to run

  1. Fully Automated Test 完全自动化
  2. Self-Checking Test 能够自我检查,说白了就是不需要人工去检查案例执行的结果对不对
  3. Repeatable Test 需要具备可重复性。鼓励开发人员在每一次按下保存按钮时都执行一次测试案例(我没事就喜欢Ctrl+S)。案例需要具备可重复性,必须具备自我清理能力(self-cleaning),通常,是在Test Fixture的TearDown中进行环境的清理。

Tests should be easy to write and maintain

  1. Simple Tests 一个首要的原则是,让测试案例尽量的简单,使得其更容易维护。一次只测一种条件(Verify One Condition per Test)
  2. Expressive Tests 测试案例要达到表达清晰的目的,比如,可以使用一些Creation Mehods和Custom Assertion。有点像BDD。
  3. 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就自动化测试的一些原则问题进行了交流,有些是显而易见的,有些又是让人把握不定。因此,这章主要讨论了以下几个问题:

  1. Test First or Last?
  2. Tests or Examples?
  3. Test-by-Test or Test All-at-Once?
  4. Outside-In or Inside-Out?
  5. State or Behavior Verification?
  6. Fixture Design Upfront or Test-by-Test?

Test First or Last?

是应该先写代码还是先写测试案例?作者认为应该先写测试案例,然后再写代码。这也是测试驱动开发和敏捷测试的一个重要原则。这样做的原因有很多,比如:

  1. 对一个已完成或旧的代码编写测试案例,比在代码完成前编写测试案例难的多。(面对一个庞大的已完成的系统时,确实会让人无从下手)
  2. 先写测试案例,可以极大的增强代码的可测性。使得后面编写的代码,天生就具备可测试的能力,因为测试案例已经早于它写好了。
  3. 先写测试案例,可以对后面的编码起到约束作用,避免编码时添加一些臃肿的、根本就不会用到的函数,使得代码看起来更加精简。

个人感受:

先编写测试案例再写代码,的确有很多好处。但是发现真正这样做的人很少,一方面,对于传统的软件开发公司,要做出一些改变确实有些困难。一方面,先编写测试案例带来的好处并不是立竿见影,很多人尝试了一下就放弃了。因此,需要不断的实践,坚持。(我也要努力了)

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?

一个测试案例一般都包含以下几个步骤:

  1. Setup
  2. Exercise
  3. Verify
  4. 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,必须非常清楚以下几个东西的关系,本文的重点也是说明一下他们之间的关系。他们分别是:

  1. Dummy Object
  2. Test Stub
  3. Test Spy
  4. Mock Object
  5. 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通常符合以下情形:

  1. 实际对象还未实现出来,先用一个简单的Fake Object代替它。
  2. 实际对象执行需要太长的时间
  3. 实际对象在实际环境下可能会有不稳定的情况。比如,网络发送数据包,不能保证每次都能成功发送。
  4. 实际对象在实际系统环境下不可用,或者很难让它变得可用。比如,使用一个依赖实际数据库的数据库访问层对象,必须安装数据库,并且进行大量的配置,才能生效。

一个使用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:

  1. It talks to the database.
  2. It communicates across the network.
  3. It touches the file system.
  4. It can’t run correctly at the same time as any of your other unit tests.
  5. You have to do special things to your environment (such as editing config files) to run it.

在讲到代码的可测性设计时,作者提出主要从以下几个方面来提高可测性:

Dependency Injection

依赖注入,代码设计中并不依赖于具体的实现,并且运行将特定的实现注入到系统中。注入主要有三种方式:

  1. Parameter Injection
  2. Constructor Injection
  3. Setter Injection

Dependency Lookup

依赖查找,运行时动态查找到所依赖的具体对象。

Humble Object

这个名称很抽象,我的理解是在代码中提供一些接口出来,方面测试。

Test Hook

在代码中加入一些测试逻辑,比如,如果是测试模块,就怎么怎么样。不过,我想这种方式应该是不值得推荐的。

最后,很欣慰在春节前把这本书看完了,可以说收获还是蛮大的。以后遇到什么问题,可以拿这本书当资料来查。xUnit Test Patterns的官方地址如下:

http://xunitpatterns.com/

可以在这个地址查询到每个测试模式和相关的名称解释。

作者:CoderZhCoderZh的技术博客 - 博客园

出处:http://coderzh.cnblogs.com/

文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。



LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...