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