Testify 提供了單測方便的斷言能力,這里的斷言是將對代碼實際返回的斷言,代碼的實際輸出和預(yù)期是否一致。下面是 gin-gonic/gin 代碼庫的單測代碼,Testify 還提供了很多其他的方法:
assert.Equal(t, "admin", user)
assert.True(t, found)
單元測試中也會存在不穩(wěn)定的代碼,我們的入?yún)㈦m然保持不變,但每次單測的結(jié)果可能會發(fā)生變化。比如說,我們會調(diào)用第三方的接口,而第三方的接口可能會發(fā)生變化。再比如,代碼中有通過 time.Now()
獲取最近7天內(nèi)的用戶訂單,這個返回結(jié)果本身就是隨當(dāng)前時間變化的。
當(dāng)然,我們肯定不希望每次單測都手動調(diào)整,來“迎合”這類不穩(wěn)定的代碼,這樣不僅疲于奔命,還效率不高。測試上使用 Mock 就可以解決這類問題。這里主要看看如何使用 Testify 的 mock 功能,從下面這個簡單的例子出發(fā):
package main
import (
"math/rand"
"time"
)
func DivByRand(numerator int) int {
rand.Seed(time.Now().Unix())
return numerator / int(rand.Intn(10))
}
DivByRand
中除以一個隨機數(shù),導(dǎo)致結(jié)果是隨機的,不可預(yù)測的,我們該如何對它進行單測呢?特別強調(diào)下,rand.Seed
方法調(diào)用是必須的,如果不隨機初始化 seed,rand.Intn
每次返回的結(jié)果都是相同的。隨機數(shù)都是基于某個 seed 的隨機數(shù),seed 不變,預(yù)期的隨機數(shù)就是固定不變的。
我們針對隨機方法的部分,做一個接口聲明,以及接口實現(xiàn),來替代代碼中隨機的被除數(shù)。這里mock需要對原函數(shù)做代碼改造,我們一起來看一下調(diào)整過程:
import (
"github.com/stretchr/testify/mock"
"testing"
)
// 聲明隨機接口
type randNumberGenerator interface {
randomInt(max int) int
}
// 聲明接口的實現(xiàn)
type standardRand struct{}
func (s standardRand) randomInt(max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max)
}
// 修改原有的方法,已經(jīng)修改了原函數(shù)的聲明
func DivByRand(numerator int, r randNumberGenerator) int {
return numerator / r.randomInt(10)
}
使用 Testify mock 功能
我們聲明一個 mock 結(jié)構(gòu)體,匿名嵌套 mock.Mock
。通過嵌套 Mock,結(jié)構(gòu)體就具備了注冊方法,返回預(yù)期結(jié)果的能力。其中,randomInt 的實現(xiàn)對應(yīng)了我們預(yù)期的輸入和輸出關(guān)系。
type mockRand struct {
mock.Mock
}
func newMockRand() *mockRand { return &mockRand{} }
func (m *mockRand) randomInt(max int) int {
args := m.Called(max)
return args.Int(0)
}
最終,我們的單測就變成了下面的樣子,其中的 On 用來對結(jié)構(gòu)體的方法 randomInt 做設(shè)置,Return 對應(yīng)了 args.Int(0)。我們執(zhí)行下面的單測,返回的結(jié)果是恒定的。
func TestDivByRand(t *testing.T) {
m := newMockRand()
m.On("randomInt", 10).Return(6)
t.Log(DivByRand(6, m))
}
Testify Mock 方法實現(xiàn)的核心就是 On 和 Return 方法了,它對應(yīng)的是我們接口的實現(xiàn)。m.Called 函數(shù)的返回值類型為 Arguments,包含了函數(shù)的返回值信息,args.Int(0)
表示獲取 Called 函數(shù)的第一個返回值。On 用來給函數(shù)設(shè)置入?yún)ⅲ琑eturn 用來給函數(shù)設(shè)置出參。
randomInt 方法只返回了一個結(jié)果,一般的函數(shù)還會額外返回 error 信息,來標志函數(shù)執(zhí)行是否出現(xiàn)異常。這種情況可以通過 args.Error(1)
來獲取,表示第二個出參是 error 類型。
從起初的單測,到最后的單測,函數(shù)的方法聲明被修改了。假設(shè)我們要處理的是線上代碼,這樣的改動其實破壞了代碼的穩(wěn)定性,為了單測,我們還需要重新對代碼做回歸測試,確保改動不會對線上環(huán)境產(chǎn)生影響。但這種改動也可以總結(jié)為一種模式,只要按照固定的模式做代碼調(diào)整就可以了:
- 針對返回結(jié)果不確定的方法 ,封裝獨立的接口聲明。
- 使用 Testify Mock 重新實現(xiàn)這個接口
- 使用 Testify Mock 確定性的實現(xiàn)來替代原來的方法。前提是將之前直接調(diào)用方法的地方,修改為接口調(diào)用。
所以,這種情況其實不利于存量代碼的覆蓋率測試,單測最好做到是不要侵入老代碼,避免引入不必要風(fēng)險。但在增量代碼上,我們可以使用這種模式,提前按照接口的模式去做功能實現(xiàn),也就是測試驅(qū)動的意思。
在開發(fā)代碼之前,先想好單測的實現(xiàn),代碼設(shè)計上多了一個維度的考量,也會讓代碼寫的更加有擴展性。
庫的其他 mock 方法
觀察下面的代碼,它用到了更多 testify 庫提供的方法,包括 MatchedBy、Once、AssertNumberOfCalls。不過就數(shù) AssertNumberOfCalls 最簡單了,用來斷言方法的調(diào)用次數(shù)。
mock.MatchedBy(reqSlothFacts)
本來應(yīng)該是傳遞函數(shù) ListAnimalFacts 入?yún)⒌模F(xiàn)在傳遞了 mock.MatchedBy 函數(shù)的執(zhí)行結(jié)果。我們詳細來看一下這幾個方法。
func TestGetSlothsFavoriteSnackOnPage2(t *testing.T) {
c := newMockClient()
c.On("ListAnimalFacts", mock.MatchedBy(reqSlothFacts)).
Return(&page1, nil).
Once()
c.On("ListAnimalFacts", mock.MatchedBy(reqSlothFacts)).
Return(&page2, nil).
Once()
favSnack, err := getSlothsFavoriteSnack(c)
if err != nil {
t.Fatalf("got error getting sloths' favorite snack: %v", err)
}
if favSnack != "hibiscus flowers" {
t.Errorf(
"expected favorite snack to be hibiscus flowers, got %s",
favSnack,
)
}
c.AssertNumberOfCalls(t, "ListAnimalFacts", 2)
}
Once 和 sync.Once 要表示的含義是一致的,表示只能執(zhí)行一次,但多次執(zhí)行 sync.Once 也是沒有問題的,只不過只有第一次生效而已。但 mock 中的 Once 執(zhí)行兩次是會報錯的。如果只是用來限定方法的執(zhí)行次數(shù),想一想,也沒啥好單測的。沿用上面的代碼,我們稍微做些改動
func TestDivByRand(t *testing.T) {
m := newMockRand()
m.On("randomInt", 10).Return(6).Once()
m.randomInt(10)
m.randomInt(10)
}
執(zhí)行上面的單測,會發(fā)生 panic,提示信息中聲明了:assert: mock: The method has been called over 1 times.
。就目前來說,難道除了 panic 就沒有什么更好的方式了?
其實 Once 還有另一個特別有用的功能,就是設(shè)置函數(shù)不同的返回值,還拿 randomInt 函數(shù)來說,如果我們期望,同樣的入?yún)?,第一次調(diào)用 randomInt 返回6, 第二次調(diào)用 randomInt 返回 5 怎么處理。注意,是相同的入?yún)?。我們可以通過這樣的方式就能輸出 6、5。
func TestDivByRand(t *testing.T) {
m := newMockRand()
m.On("randomInt", 10).Return(6).Once()
m.On("randomInt", 10).Return(5).Once()
t.Log(m.randomInt(10))
t.Log(m.randomInt(10))
}
當(dāng)然,如果不使用 Once,用下面這種不同的入?yún)?,也能達到相同的效果。改動點主要是 On 方法的第二個參數(shù),以及 Return 方法的返回值。
func TestDivByRand(t *testing.T) {
m := newMockRand()
m.On("randomInt", 10).Return(6)
m.On("randomInt", 9).Return(5)
t.Log(m.randomInt(10))
t.Log(m.randomInt(9))
}
最后,我們來看看 MatchedBy 的用法,可以用來校驗方法的入?yún)?。這個方法的使用約束比較多,參數(shù)需要是一個函數(shù),函數(shù)的返回只是 bool 類型,校驗成功返回 true,校驗失敗返回 false。函數(shù)的入?yún)⒕褪欠椒ǖ娜雲(yún)㈩愋?,且只能處理一個參數(shù),如果方法有多個參數(shù),需要聲明多個 MatchedBy。
我們用例子來看一下,我們校驗 randomInt 的參數(shù)必須等于10,如果不等于10,單測又會拋出 panic。整體來看,MatchedBy 的效用不是特別大。文章來源:http://www.zghlxwxcb.cn/news/detail-438755.html
func TestDivByRand(t *testing.T) {
m := newMockRand()
m.On("randomInt", mock.MatchedBy(func(num int) bool {
return num == 10
})).Return(6)
t.Log(m.randomInt(10))
}
本文主要是參照 Mocks in Go tests with Testify Mock 的示例,不過原文章講的過于詳細文章來源地址http://www.zghlxwxcb.cn/news/detail-438755.html
到了這里,關(guān)于Testify Mock 單元測試的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!