xUnit Test Pattern 从很多地方都有听说过,而且网上很早也有相关的学习笔记 《xUnit Test Patterns》学习笔记系列 。看到的评价都是非常不错的,甚至有点感觉是针对自动化测试代码的《设计模式》。最近终于有空阅读,阅读过程中收益不少,因此做一下笔记记录下来。
由于书的内容比较多,因此会以一个一个章节的形式记录下来
Refactoring a Test 严格意义上类似于序章,以一个实例的形式演示如何一步一步采用各种本书推荐的模式,重构一个测试用例。
注:一些带有超链接的英文术语是原文自带的,更多是想通过术语或者模式名称表达这种通用性的修改方法。如果不习惯可以略过。
原始测试用例如下:
public void testAddItemQuantity_severalQuantity_v1(){
Address billingAddress = null;
Address shippingAddress = null;
Customer customer = null;
Product product = null;
Invoice invoice = null;
try {
// Set up fixture
billingAddress = new Address("1222 1st St SW",
"Calgary", "Alberta", "T2N 2V2","Canada");
shippingAddress = new Address("1333 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
invoice = new Invoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
List lineItems = invoice.getLineItems();
if (lineItems.size() == 1) {
LineItem actItem = (LineItem) lineItems.get(0);
assertEquals("inv", invoice, actItem.getInv());
assertEquals("prod", product, actItem.getProd());
assertEquals("quant", 5, actItem.getQuantity());
assertEquals("discount", new BigDecimal("30"),
actItem.getPercentDiscount());
assertEquals("unit price",new BigDecimal("19.99"),
actItem.getUnitPrice());
assertEquals("extended", new BigDecimal("69.96"),
actItem.getExtendedPrice());
} else {
assertTrue("Invoice should have 1 item", false);
}
} finally {
// Teardown
deleteObject(invoice);
deleteObject(product);
deleteObject(customer);
deleteObject(billingAddress);
deleteObject(shippingAddress);
} }
这是一个 Obscure Test(令人费解的测试),存在以下问题:
重构能达到什么程度?大家先瞄一眼重构后的用例:
public void testAddItemQuantity_severalQuantity_v13(){
final int QUANTITY = 5;
final BigDecimal UNIT_PRICE = new BigDecimal("19.99");
final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Product product = createAProduct( UNIT_PRICE);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply(new BigDecimal(QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
LineItem expected =
createLineItem(QUANTITY, CUST_DISCOUNT_PC,
EXTENDED_PRICE, product, invoice);
assertContainsExactlyOneLineItem(invoice, expected);
}
比重构前好多了吧,而且即使是不大懂代码的人也能基本看懂用例在干嘛。那么,到底要怎么一步一步修改用例达到这个效果?接收后面各种代码的洗礼吧,骚年!
接下来从用例的每个部分逐个逐个优化
优化校验的逻辑。我们先把校验逻辑单独拿出来看:
List lineItems = invoice.getLineItems();
if (lineItems.size() == 1) {
LineItem actItem = (LineItem) lineItems.get(0);
assertEquals("inv", invoice, actItem.getInv());
assertEquals("prod", product, actItem.getProd());
assertEquals("quant", 5, actItem.getQuantity());
assertEquals("discount", new BigDecimal("30"),
actItem.getPercentDiscount());
assertEquals("unit price",new BigDecimal("19.99"),
actItem.getUnitPrice());
assertEquals("extended", new BigDecimal("69.96"),
actItem.getExtendedPrice());
} else {
assertTrue("Invoice should have 1 item", false);
}
看到末尾的 asserTrue 结合 false 是不是很诡异?这里错误地使用了 Stated Outcome Assertion(校验状态的断言), 实际应该是想只要执行到这里都要抛出一个断言失败,用 Single Outcome Assertion(单一输出的断言),即直接改成 fail
函数,语义更清晰。
List lineItems = invoice.getLineItems();
if (lineItems.size() == 1) {
LineItem actItem = (LineItem) lineItems.get(0);
assertEquals("inv", invoice, actItem.getInv());
assertEquals("prod", product, actItem.getProd());
assertEquals("quant", 5, actItem.getQuantity());
assertEquals("discount", new BigDecimal("30"),
actItem.getPercentDiscount());
assertEquals("unit price",new BigDecimal("19.99"),
actItem.getUnitPrice());
assertEquals("extended", new BigDecimal("69.96"),
actItem.getExtendedPrice());
} else {
// 改为 fail 函数
fail("Invoice should have exactly one line item");
}
接下来看下其他断言。这里断言都是硬编码了预期结果,不够直观。把预期结果抽离出来,放到一个 Expected Object 里面,可读性更强
List lineItems = invoice.getLineItems();
if (lineItems.size() == 1) {
// 添加 expected 对象,表示预期对象
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.96"));
LineItem actItem = (LineItem) lineItems.get(0);
// 所有断言都是校验预期对象和实际对象是否一致
assertEquals("invoice", expected.getInv(),
actItem.getInv());
assertEquals("product", expected.getProd(),
actItem.getProd());
assertEquals("quantity",expected.getQuantity(),
actItem.getQuantity());
assertEquals("discount",
expected.getPercentDiscount(),
actItem.getPercentDiscount());
assertEquals("unit pr", new BigDecimal("19.99"),
actItem.getUnitPrice());
assertEquals("extend pr",new BigDecimal("69.96"),
actItem.getExtendedPrice());
} else {
fail("Invoice should have exactly one line item");
}
这里写这么多有点长,不如直接把预期对象和实际对象做一次 equal 断言?
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");
}
好了,再看回来。用例里有个 if 并不是什么好事,使用 Conditional Test Logic 意味着我们还需要判断实际执行用的到底是 if 还是 else 分支。幸运的是,有一个 Guard Assertion 模式刚好就是设计来处理这样的问题的。
List lineItems = invoice.getLineItems();
assertEquals("number of items", 1,lineItems.size());
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.96"));
LineItem actItem = (LineItem) lineItems.get(0);
assertEquals("invoice", expected, actItem);
好了,到现在,我们已经把 12 行代码通过重构优化到 5 行了,还有优化空间吗?有的。回想一下,我们要验证的是什么?我们要验证的是这个 List 中唯一的对象和预期相符。这个其实可以做成 Custom Assertion(自定义断言)。
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.96"));
// 两部分断言封装成一个函数
assertContainsExactlyOneLineItem(invoice, expected);
很不错了,这段代码即使不加注释,任何人都能快速看出想校验的是 invoice 里有且只有一个和 expected 一致的对象,基本足够了。现在回头看看完整的用例。
public void testAddItemQuantity_severalQuantity_v6(){
Address billingAddress = null;
Address shippingAddress = null;
Customer customer = null;
Product product = null;
Invoice invoice = null;
try {
// Set up fixture
billingAddress = new Address("1222 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
shippingAddress = new Address("1333 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
invoice = new Invoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
} finally {
// Teardown
deleteObject(invoice);
deleteObject(product);
deleteObject(customer);
deleteObject(billingAddress);
deleteObject(shippingAddress);
} }
简单小结下我们的优化过程:
assertTrue("xxx", false);
改成了 fail('xxx')
(Stated Outcome Assertion 改成 Single Outcome Assertion)最终 12 行代码精简成 3 行(后面会讲最后一个优化项封装带来的额外代码还有其他好处,所以不算在总代码行数中),并且可读性大大提高。
很好,校验的部分差不多了,看下其它可优化的地方吧。
接下来,我们看下 finally 这部分代码。
} finally {
// Teardown
deleteObject(invoice);
deleteObject(product);
deleteObject(customer);
deleteObject(billingAddress);
deleteObject(shippingAddress);
}
大部分现代语言里都有 try/finally 的语法,用于确保无论有没有异常,一段代码都会被执行。在测试框架中,框架会捕获用例抛出的 assert 异常并做进一步处理,所以我们要在抛出异常前完成收尾 (tearDown) 工作,也避免了捕获异常后需要进行处理然后再重抛异常这种不好的写法。
但仔细看下,这段收尾工作做得并不好。如果 deleteObject 方法抛出异常怎么办?难不成,我们要写成这样来确保所有方法都被执行?
} finally {
// Teardown
try {
deleteObject(invoice);
} finally {
try {
deleteObject(product);
} finally {
try {
deleteObject(customer);
} finally {
try {
deleteObject(billingAddress);
} finally {
deleteObject(shippingAddress);
}
}
}
}
上面这样的代码无论是写还是读都相当困难,估计真写了一定会被人打死。。。
当然,我们也可以把这段代码从 Test Method(测试方法)移动到 tearDown
方法里执行,也可以借此摆脱 try/finally 这种写法。但这解决不了根本问题:每个方法我们都得对应写一遍 tearDown(每个方法创建的对象不同,所以要销毁的对象也不同)。
我们也可以用 Shared Fixture(可以理解为多个用例共享的 fixture 。像 setUp,tearDown 这类在用例执行前后额外执行的方法一般被称为 Fixture )来避免在测试方法里创建这些要销毁的对象,但这么做会产生一些坏味道(可以理解为虽然能用,但不好用或不好看的代码),像由 Shared Fixture 多用例共用特性带来的 Unrepeatable Test 和Interacting Tests 。还会产生另一个问题,谁都不知道由 Shared Fixture 创建的对象从哪里来,要到哪里去,变成 Mystery Guests(神秘的客人,即谁都不知道这个对象后面会用来干嘛。)
最好的解决方法是用 Fresh Fixture ,同时使 tearDown 具有通用性,避免为每一个用例单独写一次 tearDown 。我们可以用存在内存里的 fixture ,这样可以被 gc 自动回收,但不适用于持久化的对象(例如把内容存到了数据库里面)。在这种情况下,扩展 Test Automation Framework(自动化测试框架),加入针对这类场景的方法会是最优解。
对于对象的销毁,我们可以在框架里面增加一个注册机制,只需要在创建对象时用把对象注册一下,那么框架会自动帮我们在 tearDown 里面把这些对象全部销毁。
首先,我们把我们创建的对象用 registerTestObject
全部注册一下:
// Set up fixture
billingAddress = new Address("1222 1st St SW", "Calgary",
"Alberta", "T2N 2V2", "Canada");
// 注册
registerTestObject(billingAddress);
shippingAddress = new Address("1333 1st St SW", "Calgary",
"Alberta","T2N 2V2", "Canada");
registerTestObject(shippingAddress);
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
invoice = new Invoice(customer);
registerTestObject(shippingAddress);
注册方法的实现就是把这些注册的对象存到一个 List 中:
List testObjects;
protected void setUp() throws Exception {
super.setUp();
testObjects = new ArrayList();
}
protected void registerTestObject(Object testObject) {
testObjects.add(testObject);
}
在 tearDown 里,我们遍历这个 List ,用 delectObject 删除每个对象即可:
public void tearDown() {
Iterator i = testObjects.iterator();
while (i.hasNext()) {
try {
deleteObject(i.next());
} catch (RuntimeException e) {
// 啥都不用干,我们只是为了确保我们会继续遍历 List 的下一个对象
}
}
}
译者注:这里的 deleteObject 需要判断对象类型进行不同的 delete 操作,才能确保前面考虑的持久化操作也能被恢复回来。具体实现方式也许可以通过在 registerTestObject 同时提供该对象的销毁方法来实现。
最终,回来看看我们的用例改成什么样了:
public void testAddItemQuantity_severalQuantity_v8(){
Address billingAddress = null;
Address shippingAddress = null;
Customer customer = null;
Product product = null;
Invoice invoice = null;
// Set up fixture
billingAddress = new Address("1222 1st St SW", "Calgary",
"Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
shippingAddress = new Address("1333 1st St SW", "Calgary",
"Alberta","T2N 2V2", "Canada");
registerTestObject(shippingAddress);
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
invoice = new Invoice(customer);
registerTestObject(shippingAddress);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.96"));
assertContainsExactlyOneLineItem(invoice, expected);
}
除了每个对象都有个 registerTestObject
方法显得有点重复,我们的用例已经简化了不少。我们去掉了 try/finally 代码块,也成功修复了 deleteObject
出错会导致对象没有全部销毁的隐藏 bug 。还能再优化下吗?能!
前面我们为了确保 finally 里面 delectObject 尽量不出错,给每个变量的初始化值都是 null ,确保每个变量都存在。既然现在不用顾虑这个问题了,那么我们可以把这个 null 干掉了:
public void testAddItemQuantity_severalQuantity_v9(){
// Set up fixture
Address billingAddress = new Address("1222 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
Address shippingAddress = new Address("1333 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
registerTestObject(shippingAddress);
Customer customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
Product product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
Invoice invoice = new Invoice(customer);
registerTestObject(shippingAddress);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
LineItem expected =
new LineItem(invoice, product, 5,
new BigDecimal("30"),
new BigDecimal("69.95"));
assertContainsExactlyOneLineItem(invoice, expected);
}
很好,从干掉 finally 的角度做优化,最终也起到了不错的优化效果。简单小结下:
registerTestObject
方法,给所有用例的通用 tearDown 添加了统一销毁所有注册过的 TestObject 的代码虽然从代码行数角度,优化效果不如前面的校验逻辑明显。但从可读性和用例编写便利性的角度,得到了很大的提高。
好了,断言校验和 tearDown 我们都优化过了,该来到最后的 setUp 了。
按照惯例,先看看 setUp 的代码
// Set up fixture
Address billingAddress = new Address("1222 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
registerTestObject(billingAddress);
Address shippingAddress = new Address("1333 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
registerTestObject(shippingAddress);
Customer customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
registerTestObject(shippingAddress);
Product product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
registerTestObject(shippingAddress);
Invoice invoice = new Invoice(customer);
registerTestObject(shippingAddress);
每个对象都要写个 registerTestObject 是不是很烦?很好,我们把对象的构造方法和注册方法封装到 Creation Method(创建方法)里面吧。
// Set up fixture
Address billingAddress =
createAddress( "1222 1st St SW", "Calgary", "Alberta",
"T2N 2V2", "Canada");
Address shippingAddress =
createAddress( "1333 1st St SW", "Calgary", "Alberta",
"T2N 2V2", "Canada");
Customer customer =
createCustomer( 99, "John", "Doe", new BigDecimal("30"),
billingAddress, shippingAddress);
Product product =
createProduct( 88,"SomeWidget",new BigDecimal("19.99"));
Invoice invoice = createInvoice(customer);
这样顺眼多了。而且还带来了一个好处:用例中对象的创建方法与被测系统的 API 解耦。一旦被测系统中这个对象的构造方法做了修改,我们只需要统一改 Creation Method 即可。
改完后,这里还是有不少问题。最突出的问题就是每个构造方法参数太多,我们很难看出这些对象和本次测试的直接关系。address 的具体地点和这次测试有关吗?这个用例到底要检查这些构造函数里面这么多参数中的哪些?
此外,这里用了 Hard-Coded Test Data ,测试数据都是固定的。如果这些数据都是在数据库中创建且要求唯一时,就会造成 Unrepeatable Test ,Interacting Test 甚至 Test Run War(用例乱序执行时相互打架)。
解决方法,还是在 Creation Method 里做文章。这些烦人、且值是啥和我们这次用例无关的数据,我们都可以放在 Creation Method 里自动生成。经过调整,现在的代码长这样
final int QUANTITY = 5;
// Set up fixture
Address billingAddress = createAnAddress();
Address shippingAddress = createAnAddress();
Customer customer = createACustomer(new BigDecimal("30"),
billingAddress, shippingAddress);
Product product = createAProduct(new BigDecimal("19.99"));
Invoice invoice = createInvoice(customer);
// 其中一个 Creation Method 的实现示例
private Product createAProduct(BigDecimal unitPrice) {
BigDecimal uniqueId = getUniqueNumber();
String uniqueString = uniqueId.toString();
return new Product(uniqueId.toBigInteger().intValue(),
uniqueString, unitPrice);
}
这种模式我们细分为 Anonymous Creation Method(匿名的创建方法,表示我们只需要一个特定类型的对象,不关心对象的所有实际值)。当然,如果有个别参数和测试用例息息相关,我们可以将其传入,就像 createAProduct
那样做。
现在好多了,至少不用猜构造方法里那些参数有啥意义了。但还没完。仔细想下,address 和 customer 和我们本次测试有什么关系不?完全没有!所以,我们可以进一步封装,把他们的存在隐藏到 createACustomer
方法中:
// Set up fixture
Customer cust = createACustomer(new BigDecimal("30"));
Product prod = createAProduct(new BigDecimal("19.99"));
Invoice invoice = createInvoice(cust);
很好,这样就很清晰了。和测试相关的只有 customer,product 及 invoice 三个对象,以及 production 的单位价格、custom 的折扣值。其它无关的对象我们压根不知道它们的存在,所以也不会扰乱视线了。
再来看看这三行代码,什么地方是你看不明白的?对了,就是 new BigDecimal("30")
和 new BigDecimal("19.99")
,我们看不懂这两个数字代表什么。没关系,给这些值加个名字就好了:
public void testAddItemQuantity_severalQuantity_v13(){
final int QUANTITY = 5;
final BigDecimal UNIT_PRICE = new BigDecimal("19.99");
final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Product product = createAProduct( UNIT_PRICE);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply(new BigDecimal(QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
LineItem expected =
new LineItem(invoice, product, QUANTITY,
CUST_DISCOUNT_PC, EXTENDED_PRICE);
assertContainsExactlyOneLineItem(invoice, expected);
}
可以留意到,把这些值给了变量后,通过变量名我们可以非常直观的看到同一个值在哪些地方用到,以及这些值代表什么。为了统一,校验断言部分我们也做了相同的操作,通过计算公式,我们也说明了 EXTENDED_PRICE 如何得来。
最后,小结一下:
代码行数又大幅度减少了,是不是很棒?
最后,来对比下重构前后的测试用例把。
重构前:
public void testAddItemQuantity_severalQuantity_v1(){
Address billingAddress = null;
Address shippingAddress = null;
Customer customer = null;
Product product = null;
Invoice invoice = null;
try {
// Set up fixture
billingAddress = new Address("1222 1st St SW",
"Calgary", "Alberta", "T2N 2V2","Canada");
shippingAddress = new Address("1333 1st St SW",
"Calgary", "Alberta", "T2N 2V2", "Canada");
customer = new Customer(99, "John", "Doe",
new BigDecimal("30"),
billingAddress,
shippingAddress);
product = new Product(88, "SomeWidget",
new BigDecimal("19.99"));
invoice = new Invoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, 5);
// Verify outcome
List lineItems = invoice.getLineItems();
if (lineItems.size() == 1) {
LineItem actItem = (LineItem) lineItems.get(0);
assertEquals("inv", invoice, actItem.getInv());
assertEquals("prod", product, actItem.getProd());
assertEquals("quant", 5, actItem.getQuantity());
assertEquals("discount", new BigDecimal("30"),
actItem.getPercentDiscount());
assertEquals("unit price",new BigDecimal("19.99"),
actItem.getUnitPrice());
assertEquals("extended", new BigDecimal("69.96"),
actItem.getExtendedPrice());
} else {
assertTrue("Invoice should have 1 item", false);
}
} finally {
// Teardown
deleteObject(invoice);
deleteObject(product);
deleteObject(customer);
deleteObject(billingAddress);
deleteObject(shippingAddress);
} }
重构后
public void testAddItemQuantity_severalQuantity_v13(){
final int QUANTITY = 5;
final BigDecimal UNIT_PRICE = new BigDecimal("19.99");
final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");
// Set up fixture
Customer customer = createACustomer(CUST_DISCOUNT_PC);
Product product = createAProduct( UNIT_PRICE);
Invoice invoice = createInvoice(customer);
// Exercise SUT
invoice.addItemQuantity(product, QUANTITY);
// Verify outcome
final BigDecimal BASE_PRICE =
UNIT_PRICE.multiply(new BigDecimal(QUANTITY));
final BigDecimal EXTENDED_PRICE =
BASE_PRICE.subtract(BASE_PRICE.multiply(
CUST_DISCOUNT_PC.movePointLeft(2)));
LineItem expected =
createLineItem(QUANTITY, CUST_DISCOUNT_PC,
EXTENDED_PRICE, product, invoice);
assertContainsExactlyOneLineItem(invoice, expected);
}
明显看出,重构后逻辑清晰了,读起来也畅快,也达到了 Tests as Documentation(测试即文档)的效果。现在再来回答这个测试到底是干嘛的问题,应该轻松多了吧?
这里只说明文中用到的术语列表。更详细的的术语列表及说明可以看官方网站:http://xunitpatterns.com/XUnit%20Basics.html
令人费解的测试。详情请看 http://xunitpatterns.com/Obscure%20Test.html
根据状态输出的断言。如 asserTrue ,根据输入值是否为 true 会有不同的输出。详情请看 http://xunitpatterns.com/Assertion%20Method.html
单一输出的断言。如 fail ,固定抛出失败的异常。详情请看 http://xunitpatterns.com/Assertion%20Method.html
预期对象。即把所有预期结果放到一个带有 expected 名称的对象或变量中,断言时更方便。
带有条件判断的测试逻辑,常见于新手写的测试用例(if 成功,xxx,else xxx)。详情请看 http://xunitpatterns.com/Conditional%20Test%20Logic.html
告警断言,断言失败出现的是自定义的告警信息,而非默认的 xx 条件不成立。详情请看 http://xunitpatterns.com/Guard%20Assertion.html
自定义断言,为了用例中的特殊场景自行封装的断言。详情请看 http://xunitpatterns.com/Assertion%20Method.html
测试方法,即去掉了 setUp 和 tearDown 部分的测试用例部分代码。详情请看 http://xunitpatterns.com/Test%20Method.html
共享 Fixture 。一般是多个用例共用的 setUp ,tearDown 等。详情请看 http://xunitpatterns.com/Shared%20Fixture.html
创建方法。相比构建方法,创建方法会做进一步的封装,而且创建方法属于测试代码的一部分,而不是像构建方法那样属于被测系统的一部分,解耦性更强。详情请看 http://xunitpatterns.com/Creation%20Method.html
硬编码测试数据,引起 Obscure Test(令人费解的测试)的原因之一。详情请看 http://xunitpatterns.com/Obscure%20Test.html
测试执行战争,当多个人以随机的顺序运行这个用例时,这个用例有可能因此失败。属于 Erratic Test(不稳定的测试)的现象之一。详情请看 http://xunitpatterns.com/Erratic%20Test.html
匿名创建方法,创建方法的一个子类。详情请看:http://xunitpatterns.com/Creation%20Method.html
还要两个小节 Writing More Tests 和 Further Compaction ,主要说明这次重构抽离的方法不仅能让这个用例得益,还能让其他测试这个模块的用例都得益,所以是值得投入的。更详细的内容后面另外开一篇文章补上吧。
关于术语,目前还没完全梳理完毕,所以只把和本文相关的术语放上来。后续梳理好后再全部补充上来。
PS:做个小调查,这个重构里展现的不好的写法大家都有中枪不?