1. 程式人生 > >如何寫一個好的測試

如何寫一個好的測試

部落格原文地址

背景

在上一個專案上,由於專案成員大部分是新入職的同事,所以對於測試不是很熟悉,
這就導致了在專案前期,專案上的很多測試都不太make sense,雖然沒有什麼定量的東西來描述,
但是總結起來就2個點:

  1. 測試的名字比較模糊。
  2. 測試程式碼不易讀。

深入剖析

測試名字比較模糊

對於這一個問題,是因為很多剛開始寫測試的開發腦子裡不會快就想到given、when、then這三個詞,
一般我們寫測試寫得比較多的同事,都會使用should_return_xxxx_when(if)_xxxx。其實就是在腦子中
想到了given、when、then。而新同事照著模仿的時候,很可能就單純地寫了should_xxx

。他們只考慮了結果,
但是沒有考慮條件,這就導致了讀名字還是get不到測試想要幹什麼。

對於這個問題,我想起了多年前在stackoverflow上面看到的一種寫測試名字的模板,他是這樣推薦的:
GivenA_WhenB_C,我覺得這中寫法挺好的,所以在專案中用了起來,結合實際使用,我又對此提出了兩個改進:

  1. given when這兩個詞可以省略,當然前提是我們約定了最前面就是given,中間就是when,最後就是then。
    通過施加規則限制來縮短測試名。
  2. 測試名字常常很長,一大堆駝峰其實比較不容易閱讀。所以可以使用蛇形命名法,但是這樣就需要想一個符號來
    分隔given、when、then了,我選擇使用___
    (也就是3個_),因為經過試驗,發現2個_太短了不容易一眼看出分隔
    4個則又沒有必要。

最終的效果是這樣的(這是一個例子,來自最近我參加的DDD進階培訓中的訓練題):

parking_order_is_natural_order___park_cars_by_parking_assistant___car_parked_to_correct_parking_lot_in_turn
car_is_already_took___take_back_car_with_used_receipt___exception_is_thrown
parking_lot_is_available___park_a_car_by_parking_assistant___receipt_returned
car_is_parked___take_back_car_with_invalid_receipt___exception_is_thrown

這裡還有一個小技巧,如果一個測試真的沒有given的時候,或given不重要的時候,可以省略,但是___不能省略:

___xxx_xxx___xxx_xxx

可以總結一下這種命名方式的優點:

  1. 能制定一個規則的話,專案上的測試標題能更容易統一(可以說是統一語言了)。還可以加上靜態檢查,使得一些名字不規範的測試提前被發現
    名字不規範,說明是新進專案的同事寫的,確實要重點檢查一下。
  2. 強制規定出given、when、then,那麼,我們在寫測試的時候,就會被強迫想清楚我們的測試要做什麼。
  3. 結構化的東西更適合大腦閱讀,讀測試的時候更容易,我們不需要先讀一遍測試名才能提取到given、when、then,可以一眼就定位出三個部分。

當然,也是有缺點的:
我用這種方式寫出來的測試名字一般都比較長,這個有可能是我用詞還不夠精煉,在given、when的部分可能有時候是有一部分重複的
所以也需要刻意練習,學會精簡用詞。

最後,給這種命名方式命個名吧,就叫GWT測試命名法好了。

測試程式碼不易讀

我在專案上發現,很多人習慣在構建fake資料的時候直接將所有資訊遮蔽,就提供一個createX()的方法,
在createX的方法裡面可能還要構建X的構成部分:

X createX() {
Y yyy = new Y;
X xxx = new X(YYY, field1, field12);
return xxx;
}

除了createX外還有createAX、createBX,然後在不同的測試裡面混合呼叫。
作為看測試的我,我就很難看到到底需要一個怎樣的場景,field1到底是怎麼設定的,field2到底是怎麼設定
的,而且在想改一下createX的時候,還會牽扯到其他的測試莫名其妙就掛了。

我目前使用的解決方式是這樣的:

  1. 先要確定好測試要涉及到的重要資訊,比如上面如果field1,field2都會對測試的邏輯起到作用,
    那麼,即使冗餘,也一定要寫在測試方法內。比如:

    @Test
    void ___xxx___xxx() {
    Y y = createY();
    X x = createX(field1, field12, y)
    // do some thing
    // assert some thing
    }

    這麼做的好處是,當我要看某一個測試的時候,它的前置資料我一眼就能看出來,不需要不停地command+b跟進去才知道需要什麼。

  2. 在應用第一條原則的時候,一定要記得,只羅列出這個測試所關心的資料。假如field3完全不參與這次
    邏輯的處理,又必須要有值,那麼,在createX內部給個預設值即可,不需要放在createX引數列表中。

為什麼不適用builder?

其實之前也試過用builder,但是看起來太多了,適用create的方式能縮短要寫得程式碼行數,更容易一眼看完測試