ZOMBIES:我的軟件開發(fā)和測(cè)試簡(jiǎn)便指南(一)
很久以前,在我還是一個(gè)萌新程序員的時(shí)候,我們?cè)?jīng)被分配一大批工作。我們每個(gè)人都被分配了一個(gè)編程任務(wù),然后回到自己的小隔間里噼里啪啦地敲鍵盤。我記得團(tuán)隊(duì)里的成員在自己的小隔間里一呆就是幾個(gè)小時(shí),為打造無(wú)缺陷的程序而奮斗。當(dāng)時(shí)流行的思想是:能一次性做得越多,能力越強(qiáng)。
對(duì)于我來(lái)說(shuō),能夠長(zhǎng)時(shí)間編寫或者修改代碼而不用中途停下來(lái)檢驗(yàn)這些代碼是否有效,就像榮譽(yù)勛章一樣。那個(gè)時(shí)候我們都認(rèn)為停下來(lái)檢驗(yàn)代碼是否工作是能力不足的表現(xiàn),菜鳥才這么干。一個(gè)“真正的開發(fā)者”應(yīng)該能一口氣構(gòu)建起整個(gè)程序,中途不用停下來(lái)檢查任何東西!
然而事與愿違,當(dāng)我停止在開發(fā)過(guò)程中測(cè)試自己的代碼之后,來(lái)自現(xiàn)實(shí)的檢驗(yàn)狠狠地打了我的臉。我的代碼要么無(wú)法通過(guò)編譯,要么構(gòu)建失敗,要么無(wú)法運(yùn)行,或者不能按預(yù)期處理數(shù)據(jù)。我不得不在絕望中掙扎著解決這些煩人的問(wèn)題。
避開喪尸群
如果你覺(jué)得舊的工作方式聽起來(lái)很混亂,那是因?yàn)樗_實(shí)是這樣的。我們一次性處理所有的任務(wù),在問(wèn)題堆里左砍右殺,結(jié)果只是引出更多的問(wèn)題。著就像是跟一大群?jiǎn)适g的戰(zhàn)斗。
如今我們已經(jīng)學(xué)會(huì)了避免一次性做太多的事情。在最初聽到一些專家推崇避免大批量地開發(fā)的好處時(shí),我覺(jué)得這很反直覺(jué),但我已經(jīng)從過(guò)去的犯錯(cuò)中吸取了教訓(xùn)。我使用被 James Grenning 稱為 ZOMBIES 的方法來(lái)指導(dǎo)我的軟件開發(fā)工作。
ZOMBIES 方法來(lái)救援!
ZOMBIES 表示以下首字母縮寫:
- Z – 最簡(jiǎn)場(chǎng)景(Zero)
- O – 單元素場(chǎng)景(One)
- M – 多元素場(chǎng)景(Many or more complex)
- B – 邊界行為(Boundary behaviors)
- I – 接口定義(Interface definition)
- E – 處理特殊行為(Exercise exceptional behavior)
- S – 簡(jiǎn)單場(chǎng)景用簡(jiǎn)單的解決方案(Simple scenarios, simple solutions)
我將在本系列文章中對(duì)它們進(jìn)行分析講解。
最簡(jiǎn)場(chǎng)景
最簡(jiǎn)場(chǎng)景指可能出現(xiàn)的最簡(jiǎn)單的情況。
人們傾向于最開始的時(shí)候使用硬編碼值,因?yàn)檫@是最簡(jiǎn)單的方式。通過(guò)在編碼活動(dòng)中使用硬編碼值,可以快速構(gòu)建出一個(gè)能即時(shí)反饋的解決方案。不需要幾分鐘,更不用幾個(gè)小時(shí),使用硬編碼值讓你能夠馬上與正在構(gòu)建的系統(tǒng)進(jìn)行交互。如果你喜歡這個(gè)交互,就朝這個(gè)方向繼續(xù)做下去。如果你發(fā)現(xiàn)不喜歡這種交互,你可以很容易拋棄它,根本沒(méi)有什么可損失。
本系列文章將以構(gòu)建一個(gè)簡(jiǎn)易的購(gòu)物系統(tǒng)的后端 API 為例進(jìn)行介紹。該服務(wù)提供的 API 允許用戶創(chuàng)建購(gòu)物筐、向購(gòu)物筐添加商品、從購(gòu)物筐移除商品、計(jì)算商品總價(jià)。
首先,創(chuàng)建項(xiàng)目的基本結(jié)構(gòu)(將購(gòu)物程序的代碼和測(cè)試代碼分別放到 app
和 tests
目錄下)。我們的例子中使用開源的 xUnit 測(cè)試框架。
現(xiàn)在擼起你的袖子,在實(shí)踐中了解最簡(jiǎn)場(chǎng)景吧!
[Fact]
public void NewlyCreatedBasketHas0Items() {
var expectedNoOfItems = 0;
var actualNoOfItems = 1;
Assert.Equal(expectedNoOfItems, actualNoOfItems);
}
這是一個(gè)偽測(cè)試,它測(cè)試的是硬編碼值。新創(chuàng)建的購(gòu)物筐是空的,所以購(gòu)物筐中預(yù)期的商品數(shù)是 0。通過(guò)比較期望值和實(shí)際值是否相等,這個(gè)預(yù)期被表示成一個(gè)測(cè)試(或者稱為斷言)。
運(yùn)行該測(cè)試,輸出結(jié)果如下:
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.57] tests.UnitTest1.NewlyCreatedBasketHas0Items [FAIL]
X tests.UnitTest1.NewlyCreatedBasketHas0Items [4ms]
Error Message:
Assert.Equal() Failure
Expected: 0
Actual: 1
[...]
這個(gè)測(cè)試顯然無(wú)法通過(guò):期望商品數(shù)是 0,但是實(shí)際值被硬編碼為了 1。
當(dāng)然,你可以馬上把硬編碼的值從 1 改成 0,這樣測(cè)試就能通過(guò)了:
[Fact]
public void NewlyCreatedBasketHas0Items() {
var expectedNoOfItems = 0;
var actualNoOfItems = 0;
Assert.Equal(expectedNoOfItems, actualNoOfItems);
}
與預(yù)想的一樣,運(yùn)行測(cè)試,測(cè)試通過(guò):
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 1.0950 Seconds
你也許會(huì)認(rèn)為執(zhí)行一個(gè)被強(qiáng)迫失敗的測(cè)試完全沒(méi)有意義,但是不管一個(gè)測(cè)試多么簡(jiǎn)單,確保它的可失敗性是絕對(duì)有必要的。只有這樣才能夠保證如果在后續(xù)工作中不小心破壞了程序的處理邏輯時(shí)該測(cè)試能夠給你相應(yīng)的警告。
現(xiàn)在停止偽造數(shù)據(jù),將硬編碼的值替換成從 API 中獲取的值。我們已經(jīng)構(gòu)造了一個(gè)能夠可靠地失敗的測(cè)試,它期望一個(gè)空的購(gòu)物筐中有 0 個(gè)商品,現(xiàn)在是時(shí)候編寫一些應(yīng)用程序代碼了。
就跟常見的軟件建模活動(dòng)一樣,我們先從構(gòu)造一個(gè)簡(jiǎn)單的接口開始。在 app
目錄下新建文件 IShoppingAPI.cs
(習(xí)慣上接口名一般以大寫 I 開頭)。在該接口中聲明一個(gè)名為 NoOfItems()
的方法,它以 int
類型返回商品的數(shù)量。下面是接口的代碼:
using System;
namespace app {
public interface IShoppingAPI {
int NoOfItems();
}
}
當(dāng)然這個(gè)接口什么事也做不了,在你需要實(shí)現(xiàn)它。在 app
目錄下創(chuàng)建另一個(gè)文件 ShoppingAPI
。在其中將 ShoppingAPI
聲明為一個(gè)實(shí)現(xiàn)了 IShoppingAPI
的公有類。在類中定義方法 NoOfItems
返回整數(shù) 1:
using System;
namespace app {
public class ShoppingAPI : IShoppingAPI {
public int NoOfItems() {
return 1;
}
}
}
從上面代碼中你發(fā)現(xiàn)自己又在通過(guò)返回硬編碼值 1 的方式來(lái)偽造代碼邏輯。現(xiàn)階段這是一件好事,因?yàn)槟阈枰3忠磺谐?jí)無(wú)敵簡(jiǎn)單。現(xiàn)在還不是仔細(xì)構(gòu)想如何實(shí)現(xiàn)購(gòu)物筐的處理邏輯時(shí)候。這些工作后續(xù)再做!到目前為止,你只是通過(guò)構(gòu)建最簡(jiǎn)場(chǎng)景來(lái)檢驗(yàn)自己是否滿意現(xiàn)在的設(shè)計(jì)。
為了確定這一點(diǎn),將硬編碼值換成這個(gè) API 在運(yùn)行中收到請(qǐng)求時(shí)應(yīng)該返回的值。你需要通過(guò) using app;
聲明來(lái)告訴測(cè)試你使用的購(gòu)物邏輯代碼在哪里。
接下來(lái),你需要 實(shí)例化instantiate IShoppingAPI
接口:
IShoppingAPI shoppingAPI = new ShoppingAPI();
這個(gè)實(shí)例用來(lái)發(fā)送請(qǐng)求并接收返回的值。
現(xiàn)在,代碼變成了這樣:
using System;
using Xunit;
using app;
namespace tests {
public class ShoppingAPITests {
IShoppingAPI shoppingAPI = [new][3] ShoppingAPI();
[Fact]
public void NewlyCreatedBasketHas0Items() {
var expectedNoOfItems = 0;
var actualNoOfItems = shoppingAPI.NoOfItems();
Assert.Equal(expectedNoOfItems, actualNoOfItems);
}
}
}
顯然執(zhí)行這個(gè)測(cè)試的結(jié)果是失敗,因?yàn)槟阌簿幋a了一個(gè)錯(cuò)誤的返回值(期望值是 0,但是返回的是 1)。
同樣的,你也可以通過(guò)將硬編碼的值從 1 改成 0 來(lái)讓測(cè)試通過(guò),但是現(xiàn)在做這個(gè)是在浪費(fèi)時(shí)間。現(xiàn)在設(shè)計(jì)的接口已經(jīng)跟測(cè)試關(guān)聯(lián)上了,你剩下的職責(zé)就是編寫代碼實(shí)現(xiàn)預(yù)期的行為邏輯。
在編寫應(yīng)用程序代碼時(shí),你得決定用來(lái)表示購(gòu)物筐得數(shù)據(jù)結(jié)構(gòu)。為了保持設(shè)計(jì)的簡(jiǎn)單,盡量選擇 C# 中表示集合的最簡(jiǎn)單類型。第一個(gè)想到的就是 ArrayList
。它非常適合目前的使用場(chǎng)景——可以保存不定個(gè)數(shù)的元素,并且易于遍歷訪問(wèn)。
因?yàn)?nbsp;ArrayList
是 System.Collections
包的一部分,在你的代碼中需要聲明:
using System.Collections;
然后 basket
的聲明就變成這樣了:
ArrayList basket = new ArrayList();
最后將 NoOfItems()
中的因編碼值換成實(shí)際的代碼:
public int NoOfItems() {
return basket.Count;
}
這次測(cè)試能夠通過(guò)了,因?yàn)樽畛踬?gòu)物筐是空的,basket.Count
返回 0。
這也是你的第一個(gè)最簡(jiǎn)場(chǎng)景測(cè)試要做的事情。
更多案例
目前的課后作業(yè)是處理一個(gè)喪尸,也就是第 0 個(gè)喪尸。在下一篇文章中,我將帶你了解單元素場(chǎng)景和多元素場(chǎng)景。不要錯(cuò)過(guò)哦!