一個測試工程師走進一家酒吧,要了一杯啤酒;c++
一個測試工程師走進一家酒吧,要了一杯咖啡;git
一個測試工程師走進一家酒吧,要了 0.7 杯啤酒;github
一個測試工程師走進一家酒吧,要了-1 杯啤酒;golang
一個測試工程師走進一家酒吧,要了 2^32 杯啤酒;sql
一個測試工程師走進一家酒吧,要了一杯洗腳水;數據庫
一個測試工程師走進一家酒吧,要了一杯蜥蜴;編程
一個測試工程師走進一家酒吧,要了一份 asdfQwer@24dg!&*(@;安全
一個測試工程師走進一家酒吧,什麼也沒要;markdown
一個測試工程師走進一家酒吧,又走出去又從窗戶進來又從後門出去從下水道鑽進來;併發
一個測試工程師走進一家酒吧,又走出去又進來又出去又進來又出去,最後在外面把老闆打了一頓;
一個測試工程師走進一家酒吧,要了一杯燙燙燙的錕斤拷;
一個測試工程師走進一家酒吧,要了 NaN 杯 Null;
一個測試工程師衝進一家酒吧,要了 500T 啤酒咖啡洗腳水野貓狼牙棒奶茶;
一個測試工程師把酒吧拆了;
一個測試工程師化裝成老闆走進一家酒吧,要了 500 杯啤酒而且不付錢;
一萬個測試工程師在酒吧門外呼嘯而過;
一個測試工程師走進一家酒吧,要了一杯啤酒';DROP TABLE 酒吧;
測試工程師們滿意地離開了酒吧。
而後一名顧客點了一份炒飯,酒吧炸了。
上面是網上流行的一個關於測試的笑話,其主要核心思想是——你永遠沒法把全部問題都充分測試。
在軟件工程中,測試是極其重要的一環,比重一般能夠與編碼相同,甚至大大超過。那麼在 Golang 裏,怎麼樣把測試寫好,寫正確?本文將對這個問題作一些簡單的介紹。 當前文章將主要分兩個部分:
因爲篇幅問題,本文將不涉及性能測試,以後會另起一篇來談。
咱們舉個不太恰當的例子,測試也是代碼,咱們假定寫代碼時出現 bug 的機率是 p(0<p<1),那麼咱們同時寫測試的話,兩邊同時出現 bug 的機率就是(咱們認爲兩個事件相互獨立)
P(代碼出現 bug) * P(測試出現 Bug) = p^2 < p
例如 p 是 1%的話,那麼同時寫出現 bug 的機率就只有 0.01%了。
測試一樣也是代碼,有可能也寫出 bug,那麼怎麼保證測試的正確性呢?給測試也寫測試?給測試的測試繼續寫測試?
咱們定義 t(0)爲原始的代碼,任意的 i,i > 0,t(i+1)爲對於 t(i)的測試,t(i+1)正確爲 t(i)正確的必要條件,那麼對全部的 i,i>0,t(i)正確都是 t(0)正確的必要條件。。。
測試的種類有很是多,咱們這裏只挑幾個對通常開發者來講比較重要的測試,作簡略的說明。
首先是從測試方法上能夠分爲白盒測試和黑盒測試(固然還存在所謂的灰盒測試,這裏不討論)
咱們寫的單元測試通常屬於白盒測試,由於咱們對測試對象的內部邏輯有着充分了解。
從測試的維度上,又能夠分爲單元測試和集成測試:
單元測試能夠是黑盒測試,集成測試亦能夠是白盒測試
迴歸測試主要是但願維持軟件的不變性,咱們舉一個例子來講明。例如咱們發現軟件在運行的過程當中出現了問題,在 gitlab 上開啓了一個 issue。以後咱們而且定位到了問題,咱們能夠先寫一個測試(測試的名稱能夠帶上 issue 的 ID)來複現問題(該版本代碼運行此測試結果失敗)。以後咱們修復問題後,再次運行測試,測試的結果應當成功。那麼咱們以後每次運行測試的時候,經過運行這個測試,能夠保證一樣的問題不會復現。
咱們先來看一個 Golang 的代碼:
// add.go
package add
func Add(a, b int) int {
return a + b
}
複製代碼
一個測試用例能夠寫成:
// add_test.go
package add
import (
"testing"
)
func TestAdd(t *testing.T) {
res := Add(1, 2)
if res != 3 {
t.Errorf("the result is %d instead of 3", res)
}
}
複製代碼
在命令行咱們使用 go test
go test
複製代碼
這個時候 go 會執行該目錄下全部的以_test.go 爲後綴中的測試,測試成功的話會有以下輸出:
% go test
PASS
ok code.byted.org/ek/demo_test/t01_basic/correct 0.015s
複製代碼
假設這個時候咱們把 Add 函數修改爲錯誤的實現
// add.go
package add
func Add(a, b int) int {
return a - b
}
複製代碼
再次執行測試命令
% go test
--- FAIL: TestAddWrong (0.00s)
add_test.go:11: the result is -1 instead of 3
FAIL
exit status 1
FAIL code.byted.org/ek/demo_test/t01_basic/wrong 0.006s
複製代碼
會發現測試失敗。
那麼若是咱們想只測試這一個文件,輸入
go test add_test.go
複製代碼
會發現命令行輸出
% go test add_test.go
# command-line-arguments [command-line-arguments.test]
./add_test.go:9:9: undefined: Add
FAIL command-line-arguments [build failed]
FAIL
複製代碼
這是由於咱們沒有附帶測試對象的代碼,修改測試後能夠得到正確的輸出:
% go test add_test.go add.go
ok command-line-arguments 0.007s
複製代碼
一般來講咱們測試某個函數和方法,可能須要測試不少不一樣的 case 或者邊際條件,例如咱們爲上面的 Add 函數寫兩個測試,能夠寫成:
// add_test.go
package add
import (
"testing"
)
func TestAdd(t *testing.T) {
res := Add(1, 0)
if res != 1 {
t.Errorf("the result is %d instead of 1", res)
}
}
func TestAdd2(t *testing.T) {
res := Add(0, 1)
if res != 1 {
t.Errorf("the result is %d instead of 1", res)
}
}
複製代碼
測試的結果:(使用-v 能夠得到更多輸出)
% go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestAdd2
--- PASS: TestAdd2 (0.00s)
PASS
ok code.byted.org/ek/demo_test/t02_subtest/non_subtest 0.007s
複製代碼
另外一種寫法是寫成子測試的形式
// add_test.go
package add
import (
"testing"
)
func TestAdd(t *testing.T) {
t.Run("test1", func(t *testing.T) {
res := Add(1, 0)
if res != 1 {
t.Errorf("the result is %d instead of 1", res)
}
})
t.Run("", func(t *testing.T) {
res := Add(0, 1)
if res != 1 {
t.Errorf("the result is %d instead of 1", res)
}
})
}
複製代碼
執行結果:
% go test -v
=== RUN TestAdd
=== RUN TestAdd/test1
=== RUN TestAdd/#00
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/test1 (0.00s)
--- PASS: TestAdd/#00 (0.00s)
PASS
ok code.byted.org/ek/demo_test/t02_subtest/subtest 0.007s
複製代碼
能夠看到輸出中會將測試按照嵌套的結構分類,子測試的嵌套沒有層數限制,若是不寫測試名的話,會自動按照順序給予序號做爲其測試名(例如上面的#00)
有一種測試的寫法是:
tcList := map[string][]int{
"t1": {1, 2, 3},
"t2": {4, 5, 9},
}
for name, tc := range tcList {
t.Run(name, func(t *testing.T) {
require.Equal(t, tc[2], Add(tc[0], tc[1]))
})
}
複製代碼
看上去沒什麼問題,然而有一個缺點是,這個測試對 IDE 並不友好:
咱們沒法在出錯的時候對單個測試從新執行 因此推薦儘量對每一個 t.Run 都要獨立書寫,例如:
f := func(a, b, exp int) func(t *testing.T) {
return func(t *testing.T) {
require.Equal(t, exp, Add(a, b))
}
}
t.Run("t1", f(1, 2, 3))
t.Run("t2", f(4, 5, 9))
複製代碼
咱們上面的 add.go 和 add_test.go 文件都處於同一個目錄下,頂部的 package 名稱都是 add,那麼在寫測試的過程當中,也能夠爲測試啓用與非測試文件不一樣的包名,例如咱們如今將測試文件的包名改成 add_test:
// add_test.go
package add_test
import (
"testing"
)
func TestAdd(t *testing.T) {
res := Add(1, 2)
if res != 3 {
t.Errorf("the result is %d instead of 3", res)
}
}
複製代碼
這個時候執行 go test 會發現
% go test
# code.byted.org/ek/demo_test/t03_diffpkg_test [code.byted.org/ek/demo_test/t03_diffpkg.test]
./add_test.go:9:9: undefined: Add
FAIL code.byted.org/ek/demo_test/t03_diffpkg [build failed]
複製代碼
因爲包名變化了,咱們沒法再訪問到 Add 函數,這個時候咱們增長 import 便可:
// add_test.go
package add_test
import (
"testing"
. "code.byted.org/ek/demo_test/t03_diffpkg"
)
func TestAdd(t *testing.T) {
res := Add(1, 2)
if res != 3 {
t.Errorf("the result is %d instead of 3", res)
}
}
複製代碼
咱們使用上面的方式來導入包內的函數便可。 但使用了這種方式後,將沒法訪問包內未導出的函數(以小寫開頭的)。
咱們可使用強大的 testify 來方便咱們寫測試 例如上面的測試咱們能夠用這個庫寫成:
// add_test.go
package correct
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestAdd(t *testing.T) {
res := Add(1, 2)
require.Equal(t, 3, res)
/*
must := require.New(t)
res := Add(1, 2)
must.Equal(3, res)
*/
}
複製代碼
若是執行失敗,則會在命令行看到以下輸出:
% go test
ok code.byted.org/ek/demo_test/t04_libraries/testify/correct 0.008s
--- FAIL: TestAdd (0.00s)
add_test.go:12:
Error Trace: add_test.go:12
Error: Not equal:
expected: 3
actual : -1
Test: TestAdd
FAIL
FAIL code.byted.org/ek/demo_test/t04_libraries/testify/wrong 0.009s
FAIL
複製代碼
庫提供了格式化的錯誤詳情(堆棧、錯誤值、指望值等)來方便咱們調試。
對於須要測試 sql 的地方可使用 go-sqlmock 來測試
強大的對 interface 的 mock 庫,例如咱們要測試函數 ioutil.ReadAll
func ReadAll(r io.Reader) ([]byte, error)
複製代碼
咱們 mock 一個 io.Reader
// package: 輸出包名
// destination: 輸出文件
// io: mock對象的包
// Reader: mock對象的interface名
mockgen -package gomock -destination mock_test.go io Reader
複製代碼
能夠在目錄下看到 mock_test.go 文件裏,包含了一個 io.Reader 的 mock 實現 咱們可使用這個實現去測試 ioutil.Reader,例如
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := NewMockReader(ctrl)
m.EXPECT().Read(gomock.Any()).Return(0, errors.New("error"))
_, err := ioutil.ReadAll(m)
require.Error(t, err)
複製代碼
一般咱們測試服務端代碼的時候,會先啓動服務,再啓動測試。官方的 httptest 包給咱們提供了一種方便地啓動一個服務實例來測試的方法。
其餘一些測試工具能夠前往 awesome-go#testing 查找
上面介紹了測試的基本工具和寫法,咱們已經完成了「必先利其器」,下面咱們將介紹如何「善其事」。
在平時,你們寫服務的時候,基本都必須考慮併發,咱們使用 IDE 測試的時候,IDE 默認狀況下並不會主動測試併發狀態,那麼如何保證咱們寫出來的代碼是併發安全的? 咱們來舉個例子,好比咱們有個計數器,做用就是計數。
type Counter int32
func (c *Counter) Incr() {
*c++
}
複製代碼
很顯然這個計數器在併發狀況下是不安全的,那麼咱們如何寫一個測試來作這個計數器的併發測試呢?
import (
"sync"
"testing"
"github.com/stretchr/testify/require"
)
func TestA_Incr(t *testing.T) {
var a Counter
eg := sync.WaitGroup{}
count := 10
eg.Add(count)
for i := 0; i < count; i++ {
go func() {
defer eg.Done()
a.Incr()
}()
}
eg.Wait()
require.Equal(t, count, int(a))
}
複製代碼
經過屢次執行上面的測試,咱們發現有些時候,測試的結果返回 OK,有些時候測試的結果返回 FAIL。也就是說,即使寫了測試,有可能在某次測試中被標記爲經過測試。那麼有沒有什麼辦法直接發現問題呢?答案就是在測試的時候增長-race 的 flag
-race 標誌不適合 benchmark 測試
go test -race
複製代碼
這時候終端會輸出:
WARNING: DATA RACE
Read at 0x00c00001ca50 by goroutine 9:
code.byted.org/ek/demo_test/t05_race/race.(*A).Incr()
/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race.go:6 +0x6f
code.byted.org/ek/demo_test/t05_race/race.TestA_Incr.func1()
/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race_test.go:18 +0x66
Previous write at 0x00c00001ca50 by goroutine 8:
code.byted.org/ek/demo_test/t05_race/race.(*A).Incr()
/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race.go:6 +0x85
code.byted.org/ek/demo_test/t05_race/race.TestA_Incr.func1()
/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race_test.go:18 +0x66
Goroutine 9 (running) created at:
code.byted.org/ek/demo_test/t05_race/race.TestA_Incr()
/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race_test.go:16 +0xe4
testing.tRunner()
/usr/local/Cellar/go/1.15/libexec/src/testing/testing.go:1108 +0x202
Goroutine 8 (finished) created at:
code.byted.org/ek/demo_test/t05_race/race.TestA_Incr()
/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race_test.go:16 +0xe4
testing.tRunner()
/usr/local/Cellar/go/1.15/libexec/src/testing/testing.go:1108 +0x202
複製代碼
go 主動提示,咱們的代碼中發現了競爭(race)態,這個時候咱們就要去修復代碼
type Counter int32
func (c *Counter) Incr() {
atomic.AddInt32((*int32)(c), 1)
}
複製代碼
修復完成後再次伴隨-race 進行測試,咱們的測試成功經過!
golang 的測試類 testing.T 有一個方法 Parallel(),全部在測試中調用了該方法的都會被標記爲併發,可是注意,若是須要使用併發測試的結果的話,必須在外層用一個額外的測試函數將其包住:
func TestA_Incr(t *testing.T) {
var a Counter
t.Run("outer", func(t *testing.T) {
for i := 0; i < 100; i++ {
t.Run("inner", func(t *testing.T) {
t.Parallel()
a.Incr()
})
}
})
t.Log(a)
}
複製代碼
若是沒有第三行的 t.Run,那麼 11 行的打印結果將不正確
Golang 的 testing.T 還有不少別的實用方法,你們能夠本身去查看一下,這裏不詳細討論
做爲一個 gopher 平時要寫大量的 if err != nil,那麼在測試一個函數返回的 error 的時候,咱們好比有下面的例子
type I interface {
Foo() error
}
func Bar(i1, i2 I) error {
i1.Foo()
return i2.Foo()
}
複製代碼
Bar 函數但願依次處理 i1 和 i2 兩個輸入,當遇到第一個錯誤就返回,因而咱們寫了一個看起來「正確」的測試
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
type impl string
func (i impl) Foo() error {
return errors.New(string(i))
}
func TestBar(t *testing.T) {
i1 := impl("i1")
i2 := impl("i2")
err := Bar(i1, i2)
require.Error(t, err) // assert err != nil
}
複製代碼
這個測試結果「看起來」很完美,函數正確返回了一個錯誤。可是實際上咱們知道這個函數的返回值是錯誤的,因此咱們應當把測試稍做修改,將 error 看成一個返回值來校驗起內容,而不是簡單的判 nil 處理
func TestBarFixed(t *testing.T) {
i1 := impl("i1")
i2 := impl("i2")
err := Bar(i1, i2)
// 兩種寫法均可
require.Equal(t, errors.New("i1"), err)
require.Equal(t, "i1", err.Error())
}
複製代碼
這個時候咱們就能發現到,代碼中出現了錯誤,須要修復了。 同理能夠應用到別的返回值,咱們不該當僅僅作一些簡單的判斷,而應當儘量作「精確值」的判斷。
上面咱們討論過了測試返回值,輸入值一樣須要測試,這一點咱們主要結合 gomock 來講,舉個例子咱們的代碼以下:
type I interface {
Foo(ctx context.Context, i int) (int, error)
}
type bar struct {
i I
}
func (b bar) Bar(ctx context.Context, i int) (int, error) {
i, err := b.i.Foo(context.Background(), i)
return i + 1, err
}
複製代碼
咱們想要測試 bar 類是否正確在方法中調用了 Foo 方法 咱們使用 gomock 來 mock 出咱們想要的 I 接口的 mock 實現:
mockgen -package gomock -destination mock_test.go io Reader
複製代碼
接下來咱們寫了一個測試:
import (
"context"
"testing"
. "code.byted.org/ek/testutil/testcase"
"github.com/stretchr/testify/require"
)
func TestBar(t *testing.T) {
t.Run("test", TF(func(must *require.Assertions, tc *TC) {
impl := NewMockI(tc.GomockCtrl)
i := 10
j := 11
ctx := context.Background()
impl.EXPECT().Foo(ctx, i).
Return(j, nil)
b := bar{i: impl}
r, err := b.Bar(ctx, i)
must.NoError(err)
must.Equal(j+1, r)
}))
}
複製代碼
測試運行成功,但實際上咱們看了代碼發現,代碼中的 context 並無被正確的傳遞,那麼咱們應該怎麼去正確測試出這個狀況呢? 一種辦法是寫一個差很少的測試,測試中修改 context.Background()爲別的 context:
t.Run("correct", TF(func(must *require.Assertions, tc *TC) {
impl := NewMockI(tc.GomockCtrl)
i := 10
j := 11
ctx := context.WithValue(context.TODO(), "k", "v")
impl.EXPECT().Foo(ctx, i).
Return(j, nil)
b := bar{i: impl}
r, err := b.Bar(ctx, i)
must.NoError(err)
must.Equal(j+1, r)
}))
複製代碼
另外一種辦法是加入隨機測試要素。
一樣是上面的測試,咱們稍作修改
import (
"context"
"testing"
randTest "code.byted.org/ek/testutil/rand"
. "code.byted.org/ek/testutil/testcase"
"github.com/stretchr/testify/require"
)
t.Run("correct", TF(func(must *require.Assertions, tc *TC) {
impl := NewMockI(tc.GomockCtrl)
i := 10
j := 11
ctx := context.WithValue(context.TODO(), randTest.String(), randTest.String())
impl.EXPECT().Foo(ctx, i).
Return(j, nil)
b := bar{i: impl}
r, err := b.Bar(ctx, i)
must.NoError(err)
must.Equal(j+1, r)
}))
複製代碼
這樣就能夠很大程度上避免因爲固定的測試變量,致使的一些邊緣 case 容易被誤測爲正確,若是回到以前的 Add 函數的例子,能夠寫成
import (
"math/rand"
"testing"
"github.com/stretchr/testify/require"
)
func TestAdd(t *testing.T) {
a := rand.Int()
b := rand.Int()
res := Add(a, b)
require.Equal(t, a+b, res)
}
複製代碼
若是咱們修改一下以前的 Bar 的例子
func (b bar) Bar(ctx context.Context, i int) (int, error) {
ctx = context.WithValue(ctx, "v", i)
i, err := b.i.Foo(ctx, i)
return i + 1, err
}
複製代碼
函數基本相同,只是傳遞給 Foo 方法的 ctx 變成了一個子 context,這個時候以前的測試就沒法正確執行了,那麼如何來判斷傳遞的 context 是最上層的 context 的一個子 context 呢?
一個方法是在測試中,傳遞給 Bar 一個 context.WithValue,而後在 Foo 的實現中去判斷收到的 context 是否帶有特定的 kv
t.Run("correct", TF(func(must *require.Assertions, tc *TC) {
impl := NewMockI(tc.GomockCtrl)
i := 10
j := 11
k := randTest.String()
v := randTest.String()
ctx := context.WithValue(context.TODO(), k, v)
impl.EXPECT().Foo(gomock.Any(), i).
Do(func(ctx context.Context, i int) {
s, _ := ctx.Value(k).(string)
must.Equal(v, s)
}).
Return(j, nil)
b := bar{i: impl}
r, err := b.Bar(ctx, i)
must.NoError(err)
must.Equal(j+1, r)
}))
複製代碼
還有一種方法是實現 gomock.Matcher 這個 interface
import (
randTest "code.byted.org/ek/testutil/rand"
)
t.Run("simple", TF(func(must *require.Assertions, tc *TC) {
impl := NewMockI(tc.GomockCtrl)
i := 10
j := 11
ctx := randTest.Context()
impl.EXPECT().Foo(ctx, i).
Return(j, nil)
b := bar{i: impl}
r, err := b.Bar(ctx, i)
must.NoError(err)
must.Equal(j+1, r)
}))
複製代碼
randTest.Context 的主要代碼以下:
func (ctx randomContext) Matches(x interface{}) bool {
switch v := x.(type) {
case context.Context:
return v.Value(ctx) == ctx.value
default:
return false
}
}
複製代碼
gomock 會自動利用這個接口來判斷輸入參數的匹配狀況。
咱們來看下面的函數:
func foo(i int) (int, error) {
if i < 0 {
return 0, errors.New("negative")
}
return i + 1, nil
}
func Bar(i, j int) (int, error) {
i, err := foo(i)
if err != nil {
return 0, err
}
j, err = foo(j)
if err != nil {
return 0, err
}
return i + j, nil
}
複製代碼
這裏的邏輯看起來比較簡單,可是若是咱們想象 Bar 的邏輯和 foo 的邏輯都很是複雜,也包含比較多的邏輯分支,那麼測試的時候會遇到兩個問題
那麼如何解決這個問題?我這裏給你們提供一個思路,雖然可能不是最優解。有更好解法的但願可以在評論區提出。 個人思路是將 foo 函數從固定的函數變成一個可變的函數指針,能夠在測試的時候被動態替換
var foo = func(i int) (int, error) {
if i < 0 {
return 0, errors.New("negative")
}
return i + 1, nil
}
func Bar(i, j int) (int, error) {
i, err := foo(i)
if err != nil {
return 0, err
}
j, err = foo(j)
if err != nil {
return 0, err
}
return i + j, nil
}
複製代碼
因而在測試 Bar 的時候,咱們能夠替換 foo:
func TestBar(t *testing.T) {
f := func(newFoo func(i int) (int, error), cb func()) {
old := foo
defer func() {
foo = old
}()
foo = newFoo
cb()
}
t.Run("first error", TF(func(must *require.Assertions, tc *TC) {
expErr := randTest.Error()
f(func(i int) (int, error) {
return 0, expErr
}, func() {
_, err := Bar(1, 2)
must.Equal(expErr, err)
})
}))
t.Run("second error", TF(func(must *require.Assertions, tc *TC) {
expErr := randTest.Error()
first := true
f(func(i int) (int, error) {
if first {
first = false
return 0, nil
}
return 0, expErr
}, func() {
_, err := Bar(1, 2)
must.Equal(expErr, err)
})
}))
t.Run("success", TF(func(must *require.Assertions, tc *TC) {
f(func(i int) (int, error) {
return i, nil
}, func() {
r, err := Bar(1, 2)
must.NoError(err)
must.Equal(3, r)
})
}))
}
複製代碼
上面的寫法就能夠單獨分別測試 foo 和 Bar 了
寫測試的時候,咱們常常會提到一個詞,覆蓋率。那麼什麼是測試覆蓋率呢?
測試覆蓋率是在軟件測試或是軟件工程中的軟件度量,表示軟件程式中被測試到的比例。覆蓋率是一種判斷測試嚴謹程度的方式。有許多不一樣種類的測試覆蓋率: 代碼覆蓋率 特徵覆蓋率 情景覆蓋率 屏幕項目覆蓋率 模組覆蓋率 每一種覆蓋率都會假設待測系統已有存在形態基準。所以當系統有變化時,測試覆蓋率也會隨之改變。
通常狀況下,咱們能夠認爲,測試覆蓋率越高,咱們測試覆蓋的狀況越全面,測試的有效性就越高。
在 golang 中,咱們經過附加-cover 標誌,在測試代碼的同時,測試其覆蓋率
% go test -cover
PASS
coverage: 100.0% of statements
ok code.byted.org/ek/demo_test/t10_coverage 0.008s
複製代碼
咱們能夠看到當前測試覆蓋率爲 100%。
測試覆蓋率越高不等於測試正確,咱們分幾種狀況分別舉例。
這個在上面已經有所說起,能夠參考上面「正確測試返回值」的例子,在例子中,測試覆蓋率達到了 100%,可是並無正確測試出代碼的問題。
func AddIfBothPositive(i, j int) int {
if i > 0 && j > 0 {
i += j
}
return i
}
複製代碼
下面的測試用例覆蓋率達到了 100%,可是並無測試到全部的分支
func TestAdd(t *testing.T) {
res := AddIfBothPositive(1, 2)
require.Equal(t, 3, res)
}
複製代碼
func Divide(i, j int) int {
return i / j
}
複製代碼
Divide 函數並無處理除數爲 0 的狀況,而單元測試的覆蓋率是 100%
func TestAdd(t *testing.T) {
res := Divide(6, 2)
require.Equal(t, 3, res)
}
複製代碼
上面的例子說明 100%的測試覆蓋並非真的「100%覆蓋」了全部的代碼運行狀況。
測試覆蓋率的統計方法通常是: 測試中執行到的代碼行數 / 測試的代碼的總行數 然而代碼在實際運行中,每一行運行到的機率、出錯的嚴重程度等等也是不一樣的,因此咱們在追求高覆蓋率的同時,不能迷信覆蓋率。
這裏的重複書寫,能夠必定程度上認爲是「代碼複用」的反義詞。咱們主要從下面的幾方面來講。
測試用例只要不是徹底一致,那麼即使是比較雷同的測試用例,咱們均可以認爲是有意義的,沒有必要爲了代碼的精簡特意刪除,例如咱們測試上面的 Add 函數
func TestAdd(t *testing.T) {
t.Run("fixed", func(t *testing.T) {
res := Add(1, 2)
require.Equal(t, 3, res)
})
t.Run("random", func(t *testing.T) {
a := rand.Int()
b := rand.Int()
res := Add(a, b)
require.Equal(t, a+b, res)
})
}
複製代碼
雖然第二個測試看起來覆蓋了第一個測試,但沒有必要去特意刪除第一個測試,越多的測試越能增長咱們代碼的可靠性。
好比咱們有一份代碼
package add
const Value = 3
func AddInternalValue(a int) int {
return a + Value
}
複製代碼
測試爲
func TestAdd(t *testing.T) {
res := AddInternalValue(1)
require.Equal(t, 1+Value, res)
}
複製代碼
看起來很是完美,可是若是某天內部變量 Value 的值被不當心改動了,那麼這個測試沒法反應出這個改動,也就沒法及時發現這個錯誤了。若是咱們寫成
func TestAdd(t *testing.T) {
const value = 3
res := AddInternalValue(1)
require.Equal(t, 1+value, res)
}
複製代碼
就不用擔憂沒法發現常量值的變化了。