從測試的角度看拟逮,理想情況下撬统,我們的所作的全部測試都是對應(yīng)了實(shí)際的代碼,但這并不適用于實(shí)際情況敦迄,比如每次測試都去訪問數(shù)據(jù)庫恋追,或者都去加載許多和待測代碼毫無聯(lián)系的配置文件粒竖,這些不但會增加測試的時間開銷,同時也會增加測試用例的開發(fā)開銷几于。并且這樣也難以模擬一些特殊情況下的測試,比如需要特定的網(wǎng)絡(luò)接入條件沿后,或者當(dāng)天是某個特殊日期等等沿彭。
這時候我們可以用一些模擬的代碼來特?fù)Q實(shí)際代碼,從而幫助我們進(jìn)行測試尖滚。前兩天我司請來的老師來講的:Stub和Mock喉刘,就是兩種這樣的模擬代碼。
但是Stub和Mock的用法由于相似漆弄,都是為了替換外部依賴對象睦裳,從而經(jīng)常被理解混淆,或者根本沒有分清撼唾。但是實(shí)際上這是兩種完全不同的事物:
- 首先它們對于怎么去確定測試結(jié)果使用的方式是不同的廉邑,其中一個使用行為去確認(rèn)(behavior verification),一個使用狀態(tài)去確認(rèn)(state verification)
- 另一方面這是兩種將測試和設(shè)計結(jié)合在一起的方法倒谷,一個是自頂向下蛛蒙,一個是自下而上
簡言之,Stub更關(guān)注交互行為渤愁,為了驗(yàn)證待測系統(tǒng)調(diào)用目標(biāo)系統(tǒng)接口的交互行為牵祟,而Mock更關(guān)注狀態(tài),為了驗(yàn)證待測系統(tǒng)調(diào)用了目標(biāo)系統(tǒng)后抖格,目標(biāo)系統(tǒng)的狀態(tài)诺苹。
public class OrderStateTester {
private WareHouse warehouse = new WareHouseImpl();
@Before
protected void setup() throws Exception {
warehouse.setSize(50);
warehouse.setLocation("Shang Hai");
}
@Test
public void testOrderIsFilledIfEnoughInWarehouse() {
Order order = new Order(50);
order.fill(warehouse);
assertTrue(order.isFilled());
assertEquals(0, warehouse.getInventory());
}
@Test
public void testOrderDoseNotRemoveIfNotEnough() {
Order order = new Order(51);
order.fill(warehouse);
assertFalse(order.isFilled());
assertEquals(50, warehouse.getInventory());
}
}
類似這樣的TestCase是最常見的一種,可以看到我們需要測試的是Order對象雹拄。為了這個測試收奔,需要Order跟Warehouse,需要Warehouse的理由有兩個:首先需要通過它來配合測試,其次需要它來進(jìn)行驗(yàn)證(因?yàn)閛rder.fill改變了warehouse對象)
如果使用Mock對象會怎么樣呢办桨?有很多可用的mock對象庫筹淫,Mokito,JMock之類的呢撞,如果用jMock寫一段測試用例則會是:
public void testFillingRemovesInventoryIfInStock() {
Order order = new Order(50);
Mock warehouseMock = new Mock(Warehouse.class);
warehouseMock.expects(once()).method("hasInventory")
.with(eq(content),eq(50))
.will(returnValue(true));
warehouseMock.expects(once()).method("remove")
.with(eq(content), eq(50))
.after("hasInventory");
order.fill((Warehouse) warehouseMock.proxy());
warehouseMock.verify();
assertTrue(order.isFilled());
}
可以看到在數(shù)據(jù)準(zhǔn)備的階段损姜,創(chuàng)建了一個Warehouse類的mock對象,接著設(shè)置了Mock的期望殊霞,這些期望也就是在測試order時會被執(zhí)行摧阅。
在驗(yàn)證階段,和之前一樣可以跑order對象的斷言绷蹲,其次可以調(diào)用mock的verify方法棒卷,驗(yàn)證它是否像期望的那樣去運(yùn)行顾孽。
關(guān)鍵不同點(diǎn)在于怎么樣去驗(yàn)證order在于warehouse的交互中做了正確的事。上面的testcase中比规,我通過warehouse的狀態(tài)去驗(yàn)證若厚。
如果對于Stub和Mock還是分不清楚的話,或許可以通過老師舉得MailSender的case來解釋一番:如果我們有一個發(fā)送郵件的服務(wù),會和待測對象交互:
public interface MailSender {
public void send (Message msg);
}
如果使用Stub去驗(yàn)證:
public class MailSenderStub implements MailSender {
private List<Message> messages = new ArrayList<Message>();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
@Test
public void testOrder {
Order order = new Order(51);
MailSenderStub mailer = new MailSenderStub();
order.setMailer(mailer);
assertEquals(1 , mailer.numberSent());
}
我們不去關(guān)心它是否會發(fā)送給正確的人蜒什,或者發(fā)送的內(nèi)容是否正確测秸。
如果使用Mock去驗(yàn)證:
@Test
public void testOrderSendsMailIfUnFilled() {
Order order = new Order(51);
Mock mailer = mock(MailSender.class);
Mock warehouse = mock(Warehouse.class);
order.setMailer((MailSender)mailer.proxy());
warehouse.expects(once()).method("hasInventory").withAnyArgument()
.will(returnValue(false));
order.fill((Warehouse)warehouse.proxy())
}
兩種方法都用了別的代碼替代真正的MailSender,不同的是Stub采用行為驗(yàn)證灾常,只要發(fā)送了郵件即可霎冯,而Mock采用了狀態(tài)驗(yàn)證。
在重新學(xué)習(xí)了Stub和Mock之后钞瀑,之前逐漸混淆的概念又有了新的理解沈撞,對于目前維護(hù)的系統(tǒng)中測試?yán)щy的問題,比如測試一段方法需要許多l(xiāng)ogger對象雕什,或者需要查詢DB缠俺,發(fā)現(xiàn)可以通過完善Stub來解決,而且由于目前系統(tǒng)的開發(fā)背景监徘,logger等無關(guān)對象(與代碼邏輯無關(guān))的實(shí)現(xiàn)在多個項(xiàng)目中都幾乎相同晋修,所以可以用一套統(tǒng)一的Stub來實(shí)現(xiàn)多個系統(tǒng)的測試。而對于那些我們關(guān)心它狀態(tài)的依賴凰盔,例如MessageSender墓卦,則可以通過Mock的方式實(shí)現(xiàn)并驗(yàn)證。
現(xiàn)在的老項(xiàng)目流傳下來的祖?zhèn)鱰est case幾乎沒有一個能跑通的户敬,下一步的目標(biāo)就是保證新代碼的測試覆蓋率落剪,以及在力所能及的范圍里面把老代碼的測試也搞起來 : )