編寫優秀的單元測試(六)編寫可讀的測試

2021-10-03 03:01:40 字數 3273 閱讀 3501

網上有乙個段子,說程式設計師最煩做的兩件事情,乙個是寫文件,乙個是寫注釋。程式設計師最煩別人不做的兩件事情,乙個是不寫文件,乙個是不寫注釋。這裡提到了程式開發裡面的乙個最基本的事實:

讀**比寫**難。

這也是我們經常在工作中重構的原因,因為我們經常覺得有看那些**的時間,我都重寫一遍了。

說這些是為了告訴我們,要讓寫的測試**好讀,它需要比我們認為的簡單**更簡單,更傻瓜,更清晰。

抽象層次過低,斷言過於基本

過度斷言

附加細節

體現多個測試

測試方法過於冗長或過於分散

魔法數字

安裝方法過於冗長

斷言過於基本的意思是:大量使用基本斷言,一眼看不出來在斷言些什麼。我們看下面的例子:

public class baseasserttest

}public class a

}

我們寫乙個很簡單的測試程式用於說明一下這個場景,上面的例子中,我們的斷言一眼看過去不太好理解,原因是這段**並不符合我們的閱讀習慣。如果有人覺著這個**還行,能讀,那我們看看修改之後的**

[fact]

public void outtest()

看到這個**,我們立馬可以讀出來,這個斷言是看test放到a的out方法輸出出來的結果是不是包含了te字串,可讀性立馬就強了很多。

經過上面的例子我們發現,要寫出有意義的,容易讀的斷言,需要我們:

提公升抽象層次,根據要斷言的實際意義提公升**的抽象層次,使之可讀性增強

合理的使用語言特性,單元測試庫給我們提供的方法,深入了解工具能讓我們用更好的方式表達我們的測試意圖。

如果過度斷言,會讓我們的系統**現很多不明所以的斷言,同時我們在執行測試的時候會發現,經常被測試打斷。

舉個例子,如果乙個類用於從資料庫中拿出所有人員的考勤資訊,對比處理,最後輸出遲到人員的名單,程式設計師a為了省事,直接從資料庫中拿出了一千名員工的打卡資訊,然後分離出遲到人員的名單,構建輸出字串,指定斷言:

輸入總資料 - 執行 - 斷言輸出結果是不是等於構建的輸出結果

這樣做確實能達到測試的目的,但是,系統中一些微小的變化就會讓斷言失敗,比如,新加了一條需求,出差人員不打卡不算遲到,這時候我們就需要修改源資料,期望資料,以求測試通過,而在修改之前,測試就會一直通不過。我們舉的例子還是比較簡單的,試想,在乙個系統中修改了一小段**,一堆測試都無法通過,是多麼絕望。如果這些不通過的測試是有意義的,那正是我們編寫測試的意義,如果這些測試都是無意義的,那真是讓人頭大。

上面的例子告訴我們,要盡量的少些一些全量的測試,不要輸入一大串,輸出一大串,而是針對目標的進行測試。

所以我們所說的過度斷言不僅是指應該去掉無意義的斷言,減少斷言數量,更重要的是講應該提公升斷言質量。

如何提公升斷言質量,就我們之前的例子來說,我們可以做下面的思考:

附加細節是指測試中不能一目了然的部分,這部分的問題是:抽象過於複雜,以至於隱藏了真實的意思,使人不能一目了然的讀出測試的意思。出現這個問題的原因一般是抽象層次過於複雜。(我們之前講了 基本斷言 引起的原因很多時候是沒有抽象,這裡我們又講了,抽象層次不能過於複雜,那麼抽象層次如何為好呢,我們說,一目了然就好,單一的抽象層次就好)

為了將測試的附加細節去除掉,我們需要:

將一些不需要關注的環境構建,物件準備的**抽離到私有的方法,或明確標註安裝部分

命名需要恰當和具有描述性

乙個方法盡量只有乙個抽象層次

如果乙個測試,測試了多個問題,這是不好的,乙個測試只檢查一件事,不要胃口太大,這個注意和過度斷言區分。

過度斷言指的是我們要斷言的事情太大,顆粒度太大,一旦發生錯誤也不好準確的判斷錯誤位置,體現多個測試是指我們在乙個測試中斷言了多個完全不同的問題。

比如:乙個測試方法先斷言了傳入引數是否合法,再斷言了部分邏輯,而後又斷言了返回的引數是否符合預期,這是不好的。

乙個測試應當只關注一件事情。

這個也很好理解,過於冗長或過於分散(分散在多個檔案中,變數定義在專門的變數定義檔案中,測試時直接使用,不好查詢)的**編寫方式都會降低我們的可讀性,在這裡我們建議:

短小的變數定義,環境準備,直接內聯到測試方法中

較長較複雜的環境準備**,使用工廠方法,或者構造器,將其隱藏在私有方法中(這個方法的使用與我們在 附加細節 中的方法異曲同工)

如果實在沒辦法再將其拆分到單獨的檔案中

與團隊協商使用一致的構造密度及方法(如,資源放到同乙個地方,在同乙個資料夾中構造環境,變數的定義放到同乙個檔案中等)

這個意思不用解釋,解決方法也很常見:用有意義變數名的變數替換魔法數字。我們在實際的使用中:

如果該變數在很多測試都用過且變數值不變:直接定義常量

如果該變數只有該測試使用:直接定義私有變數

我們這裡著重介紹有一種減少私有變數,還可以明確變數意義的方法:

public void perfectgame()

private int pins(int n) => n;

private int times(int n) => n;

private int score(int n) => n;

使用私有方法封裝魔法數字,即保證了可讀性,也提公升了變數使用的靈活性。

我們之前講了如果測試方法過於冗長需要將安裝方法抽出,我們還需要注意,抽出的安裝方法如果還是過長,依然需要再次進行抽離和抽象,不要編寫過於冗長的安裝方法,我們可以嘗試:

抽取過於細節的**放入私有的方法

命名的時候特別小心,給與適當的,描述性的命名

在安裝中的**盡量做到屬於同一抽象層次

前兩條可能比較好理解,最後一條,「同一抽象層次」如何理解呢?簡單說 「同一抽象層次」 就是指導我們提出私有方法,這裡我講乙個我在工作中常用的方法,也是程式設計中常用的方法:由外而內,逐步細化,強化設計,弱化細節。

我舉乙個例子說一下:根據業務需要,我們需要排序展示目前系統中所有裝置的名稱,而裝置名稱來自多個源,這時候我先不關注實現細節,先把業務流程寫出來(這實際上每個程式設計師拿到需求都會做,我們需要強化這種方法,並且關注其產出):

從資料庫取出裝置名

從特定檔案中取出使用者自定義的裝置名

進行去重

進行排序

寫出了流程之後,這就是第一抽象,我們在bll層的**就是這樣的:

public void showterminal()

這個流程就是第一抽象,業務流程抽象,下面還有幾層抽象取決於業務複雜度,比如 有好幾種資料庫的儲存,我們還需要抽象一層來組裝資料庫回來的資料。

在使用這種方法的時候,我們需要注意要經常檢查,由於業務和技術的差異性,一旦發現當前的抽象層級設計有誤需要及時調整。

編寫優秀的單元測試(五)編寫測試

準備 執行 斷言 arrange act assert 這個流程是 準備用於測試的物件 觸發執行 對輸出進行斷言 怎麼寫測試是個問題,寫多少測試,也一直是個問題。這裡我們給出乙個建議 檢查行為,而非實現。具體來說,我們應該跳出具體的實現,把關注點放在我們期望類有怎樣的行為上,不應該讓實現主導測試,而...

編寫優秀的單元測試(二)測試先行

測試先行就是我們常說的測試驅動開發 tdd 測試先行的實踐方式是在接到乙個新功能的時候,先寫乙個測試,這個測試一定會失敗,然後編寫 使得測試成功,然後再寫下乙個測試,有點像是填坑的方式進行開發。傳統的開發方式是 設計 開發 測試 如此往復 測試先行的開發方式是 測試 重構 如此往復 下圖指示了設計先...

單元測試 單元測試編寫的原則

公司要求提公升單元測試的質量,其中我作為方案和推動的主導,對開發過程中的單元測試,有了一些思考和總結 單元測試編寫的目的,是面向計算機特性的,基於函式的in out,所以單元測試的好幫手就是斷言,通過不斷的構造輸出並對結果進行斷言,我們就可以針對乙個物件以及它的函式,構建出充足的用例去包裹它,以期望...