我不知道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

TestFlight内部测试 提示无可用构建版本_java

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内部的输入进行验证。

TestFlight内部测试 提示无可用构建版本_c#_02

使用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只负责获取内部情报,并把情报发出去,不负责验证情报的正确性。

TestFlight内部测试 提示无可用构建版本_java_03

使用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的验证结果。

TestFlight内部测试 提示无可用构建版本_数据库_04

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呢?