若是代碼中包含如下代碼java
或者上線後進行這種活動python
那麼這種編程方式就是祈禱式編程。golang
用流程圖表示基本就是這個樣子。編程
祈禱式編程有什麼危害呢?小程序
解決這個問題有好多種方法,單元測試是其中之一。設計模式
單元測試是由開發人員編寫的,用於對軟件基本單元進行測試的可執行的程序。
單元(unit)是一個應用程序中最小的課測試部分。(好比一個函數,一個類
google 把測試分紅小型測試、中型測試和大型測試。單元測試基本和小型測試的做用相似,可是一般也會使用mock或者stub 的方式模擬外部服務。安全
理想狀況下,單元測試應該是相互獨立、可自動化運行的。網絡
目的: 一般用單元測試來驗證代碼邏輯是否符合預期。完整可靠的單元測試是代碼的安全網
,能夠在代碼修改或重構時驗證業務邏輯是否正確,提早發現代碼錯誤,減小調試時間。設計良好的單元測試某些狀況下能夠比文檔更能反應出代碼的功能和做用。併發
單元測試這麼多優勢爲何有人不喜歡寫單元測試呢?ide
這篇文章主要關注第四個問題,如何寫單元測試。
首先看一下單元測試的結構,一個完整的單元測試主要包括Arrange-Act-Assert(3A) 三部分。
好比咱們要給下面這段代碼(golang)加單元測試:
func Add(x, y int) int { return x + y }
單元測試代碼以下:
import "testing" func TestAdd(t *testing.T) { // arrange 準備數據 x, y := 1, 2 // act 運行 got := Add(x, y) //assert 斷言 if got != 3 { t.Errorf("Add() = %v, want %v", got, 3) } }
什麼樣的單元測試纔是好的單元測試呢?
先看一個例子:
package ut import ( "fmt" "strconv" "strings" ) func isNumber(num string) (int, error) { num = strings.TrimSpace(num) n, err := strconv.Atoi(num) return n, err } func multiply(x string, y int) string { // 若是x 去除先後的空格後是數字,返回 數字的乘積 // 好比 x="2" y=3 return "6" // 若是x 去除先後的空格後不是數字,則返回字符串的x的y倍 // 好比 x="a" y=2 return "aa" num, err := isNumber(x) if err == nil { return fmt.Sprintf("%d", num*y) } result := "" for i := 0; i < y; i++ { result = fmt.Sprintf("%s%s", result, x) } return result }
測試代碼多是這個樣子。
// 測試方法的名字不直觀,並不能看出具體要測試什麼 func Test_multiply(t *testing.T) { type args struct { x string y int } // 一個測試方法中有太多的測試用例 tests := []struct { name string args args want string }{ { "return nil", args{ "", 2, }, "", }, { "return 2", args{ "1", 2, }, "2", }, {// 測試數據有點奇葩,不直觀 "return aaa", args{ "aaaaaaaaaa", 6, }, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := multiply(tt.args.x, tt.args.y); got != tt.want { // 數據錯誤的時候有明確標明測試數據,指望結果和實際結果,這一點仍是有用的 t.Errorf("multiply() = %v, want %v", got, tt.want) } }) } }
這個單元測試代碼有什麼問題呢?
結合上面咱們對單元測試目的的描述,一個好的單元測試應該知足如下幾個條件:
Test_{被測方法}_{輸入}_{指望輸出}
好比,上邊的單元測試咱們改爲這樣:
// 測試特殊值 「空字符串」 func Test_multiply_empty_returnEmpty(t *testing.T) { // 用例簡單,只包含輸入、執行和判斷 x, y, want := "", 1, "" got := multiply(x, y) if got != want { // 有效的失敗消息 t.Errorf("multiply() = %v, want %v", got, want) } } // 測試包含空格的數字 邊界值 func Test_multiply_numberWithSpace_returnNumber(t *testing.T) { x, y, want := " 2", 3, "6" got := multiply(x, y) if got != want { t.Errorf("multiply() = %v, want %v", got, want) } } // 測試正常數據 func Test_multiply_number_returnNumber(t *testing.T) { x, y, want := "2", 3, "6" got := multiply(x, y) if got != want { t.Errorf("multiply() = %v, want %v", got, want) } } // 測試非數字字符 func Test_multiply_String_returnString(t *testing.T) { // 輸入簡單的字符串就能夠測試,不必用太奇怪或者太長或者太大的數據數據 x, y, want := "a", 3, "aaa" got := multiply(x, y) if got != want { t.Errorf("multiply() = %v, want %v", got, want) } } // 測試空格 邊界值 func Test_multiply_space_returnSpace(t *testing.T) { x, y, want := " ", 3, " " got := multiply(x, y) if got != want { t.Errorf("multiply() = %v, want %v", got, want) } }
固然這個數據也並不完整,還能夠再加入:
既然好的單元測試須要能完整的測試代碼,那麼有什麼方法能夠保證單元測試能夠完整覆蓋被測代碼呢?
基於代碼路徑進行分析編寫單元測試是一個方法。
設計測試路徑時可使用流程圖的方式來分析,拿上邊multiply
的例子進行分析,這段代碼的路徑以下:
固然,每一個路徑的測試數據並非只有一種,好比x爲先後包含空格的數字字符串
這個路徑中就包含三種狀況:
合理的設計測試數據很是重要,測試除了符合上邊說的要簡單直觀之外還要着重考慮邊界值。
設計測試數據一般是把可能的輸入數據分紅多個子集,而後從每一個子集中選取具備表明性的數據做爲測試用例。
好比一段代碼的做用是計算個稅,咱們就應該按照個稅不一樣的等級來設計測試數據,好比:
而後在這個子集的基礎上在針對邊界值作一些檢查,好比36000、144000 等。
一般狀況下,若是私有方法在公有方法中有被調用,經過測試公有方法就已經能夠間接測試到私有方法。
也有些私有方法寫的不合理,好比私有方法沒有被使用或者私有方法的功能和類的相關性不大,這個時候就建議把私有方法單獨提取成新的函數或者類來測試。
固然現實世界中的代碼並不會這麼簡單,一般都會包含外部請求或者對於其它類的調用。
在編寫單元測試時,對於外部依賴咱們一般使用Mock和Stub的方式來模擬外部依賴。
Mock和Stub 的區別:
下面是代碼示例:
實際代碼:
//auth.go //假設咱們有一個依賴http請求的鑑權接口 type AuthService interface{ Login(username string,password string) (token string,e error) Logout(token string) error }
Mock代碼:
//auth_test.go type authService struct {} func (auth *authService) Login (username string,password string) (string,error){ return "token", nil } func (auth *authService) Logout(token string) error{ return nil }
在測試代碼中使用 authService實現了AuthService 接口,這樣測試時能夠模擬外部的網絡的請求,解除依賴。
這裏使用的是golang 代碼,golang 不支持重載,這樣使用的問題是會產生大量重複的代碼。
若是是python、java等支持重載的面嚮對象語言,能夠簡單的繼承父類,只重載包含外部請求的代碼就能夠實現Mock的需求。
package ut func notifyUser(username string){ // 若是是管理員,發送登陸提醒郵件 } type AuthService struct{} func (auth *AuthService) Login(username string, password string) (string, error) { notifyUser(username) return "token", nil } func (auth *AuthService) Logout(token string) error { return nil }
對於這段代碼想要測試實際上是比較困難的,由於Login 中調用了notifyUser,若是想測試這段代碼:
第二種就是stub 的方式。
經過這個例子咱們也能夠看到,若是想要代碼容易測試,代碼在設計時就應該考慮可測試性。
Writing Testable Code 中提到一個很是實用的觀點:在開發時,多想一想如何使得本身的代碼更方便去測試。若是考慮到這些,那麼一般你的代碼設計也不會太差。
若是代碼中出現瞭如下狀況,那麼一般是不易於測試的:
這篇文章地址爲:http://misko.hevery.com/attac... 推薦閱讀。
也能夠在公號回覆 「test」 獲取pdf
總結一下就是編寫可測試代碼,使用高質量單元測試(命名清晰、功能簡單、路徑完整、數據可靠)保證代碼質量。
最後,感謝女友支持和包容,比❤️
也能夠在公號輸入如下關鍵字獲取歷史文章:公號&小程序
| 設計模式
| 併發&協程