《xUnit Test Patterns》学习笔记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操作 :
// 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操作提取出来,作为一个公用的方法,提高了复用性。
// 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 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的行为对外部因素造成的影响进行验证。
比如下面这个例子:
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)
// 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
需要比较对象内部很多属性时,使用对象比较会更简单。
原有案例代码:
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());
}
改进后:
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,隐藏掉这些细节。比如:
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的操作。比如:
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的情况:
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");
}
可以看出,上面的写法是不好的,验证中有逻辑判断意味着有可能案例不够单一,使得案例难以理解。因此,比较好的是改成下面的方式:
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的方式去实现,编写一些简单的测试案例,保证辅助方法也是正确的。也就是说,测试案例代码本身也是需要被测试的。
作者:CoderZh
公众号:hacker-thinking (一个程序员的思考)
独立博客:http://blog.coderzh.com
博客园博客将不再更新,请关注我的「微信公众号」或「独立博客」。
作为一个程序员,思考程序的每一行代码,思考生活的每一个细节,思考人生的每一种可能。
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。