成都博为峰

今日技术干货:go测试包testify就这么用

发布日期:2024年09月03日

testify是一个功能比较全的go语言测试框架,同时支持...

testify是一个功能比较全的go语言测试框架,同时支持了断言、mock、套件等功能。原生兼容go语言testing包,单看某个功能可能不是*好的,但是整体上来看,testify的综合实力非常强。01起步testify的使用方式非常简单,基本上和go原生的testing包一样,引入包后直接使用就行,让我们一起看下。假设我们的main包里有如下代码:package mainfunc Add(a, b int) int { return a + b}下面我们针对此Add函数进行测试,添加测试文件main_test.gopackage mainimport ( "testing" "github.com/stretchr/testify/assert")func TestAdd(t *testing.T) { // 原生写法 // got := Add(2, 2) // want := 4 // if got != want { // t.Errorf("got %q, want %q", got, want) // } assert.Equal(t, Add(2, 2), 4, "Add(2,2) should be 4")}上面使用了testify的断言,断言的用法非常简单,直接调用assert.Equal(t, got, want, "message")即可。直接在命令行执行go test -v就能看到test相关输出。非常简单,在使用上基本和go原生的testing包一样。下面我们继续探索。02断言1. assert断言testify提供了方便的断言功能,这相比原生的got != want,got == want这种断言方式,更加清晰易读。断言方式特别多,这里仅介绍常用的。// 特点:最后一个参数都是断言的描述assert.Equal(t, Add(2, 2), 4, "Add(2,2) should be 4")// 不等于assert.NotEqual(t, 3, 5)// true or falseassert.True(t, true, "should is true")assert.False(t, false, "should is false")// nilassert.Nil(t, nil)// contains 包含// 字符串assert.Contains(t, "hello world", "world")// 数组assert.Contains(t, [3]int{1, 2, 3}, 2)// mapassert.Contains(t, map[string]int{"a": 1, "b": 2}, "a")// sliceassert.Contains(t, []string{"a", "b", "c"}, "b")// errorassert.Error(t, errors.New("a error"), errors.New("a error"))// emptyassert.Empty(t, []string{})assert.Empty(t, map[string]int{})assert.Empty(t, "")// zero 它检查的是 是否为0值assert.Zero(t, 0)assert.Zero(t, 0.0)assert.Zero(t, false)2. require断言除了assert断言外,testify还提供了require断言,它和assert断言类似,assert支持的函数,require也都支持。它们的区别在于,在一个测试函数中,如果断言失败,是否会继续执行;assert断言失败,测试函数继续执行;require断言失败,测试函数直接退出;PS:是否继续执行,指的是当前测试函数。而不是影响其它函数是否执行。我们编写如下测试代码,加以说明。// require_test.gopackage mainimport ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require")func TestRequire(t *testing.T) { require.Equal(t, 2, 1) fmt.Println("==由于前面require失败,所以这里不会执行=====")}func TestAssert(t *testing.T) { assert.Equal(t, 2, 1) fmt.Println("===虽然前面assert会失败 但是仍会继续执行===")}func TestOther(t *testing.T) { assert.Equal(t, 1, 1) fmt.Println("====其它测试函数照样执行=======")}执行结果如下:dongmingyan@pro ? ~/go_playground/hello ? go test require_test.go--- FAIL: TestRequire (0.00s) require_test.go:12: Error Trace: /Users/dongmingyan/go_playground/hello/require_test.go:12 Error: Not equal: expected: 2 actual : 1 Test: TestRequire===虽然前面assert会失败 但是仍会继续执行===--- FAIL: TestAssert (0.00s) require_test.go:17: Error Trace: /Users/dongmingyan/go_playground/hello/require_test.go:17 Error: Not equal: expected: 2 actual : 1 Test: TestAssert====其它测试函数照样执行=======FAILFAIL command-line-arguments 0.507sFAIL符合我们的预期。3. 断言简写我们可以看到前面的断言函数在执行时每次都需要传递testing.T参数,这有点麻烦,我们可以优化下。// 先New一个assertassert := assert.New(t)// 然后就可以不带t了assert.Equal(1, 1)03mock除了断言以外,难能可贵的是,testify还支持mock;相比于gomock的繁琐,testify相比来说简单了不少。1. 什么是mock呢?其实就是这个mock单词的中文含义——模拟,我们在代码中,通常有很多外部依赖,比如数据库、网络请求等,这些外部的依赖我们无法直接控制,所以需要mock。通过mock来模拟这些依赖,让我们的测试只关心我们代码的功能,而不必关心外部的依赖项。2. 怎么用?假设我们有如下main.go代码package mainimport "fmt"type User struct { ID int Name string}// 一个外部的接口type Server interface { // 有一个GetUser的方法 返回User GetUser(id int) User}// 打印用户的信息func GetUserInfo(server Server, id int) string { // 依赖于外部的接口Server的GetUser方法 user := server.GetUser(id) return fmt.Sprintf("user id is %d, name is %s", user.ID, user.Name)}我们需要测试GetUserInfo这个函数,但是这个函数依赖了一个接口的GetUser方法,我们可以通过mock来实现测试。main_test.go代码如下:package mainimport ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock")// 第一步:定义一个mock结构体type ServerMock struct { mock.Mock}// 第二步:定义一个mock方式(固定的)func (m *ServerMock) GetUser(id int) User { // 这里Called参数要原封不动给到 args := m.Called(id) // 返回值args.Get(0)是一个interface Get(0)代表第*个参数 return args.Get(0).(User)}func TestPrintUserInfo(t *testing.T) { // 创建我们事先定义好的mock对象 server := &ServerMock{} // 第三步:设定mock方法的的传参数和返回值 server.On("GetUser", 1).Return(User{1, "Tom"}) uinfo := GetUserInfo(server, 1) // 断言 assert.Equal(t, "user id is 1, name is Tom", uinfo)}上面的代码已经写了详尽的注释,就不做不过多解释了;我们可以总结的是,testify的操作步骤也就三步:step1:定义一个mock结构体step2:定义一个mock方式(固定的)step3:设定mock方法的的传参数和返回值3. mock代码要求我们先看一个需要测试的函数:// 除一个随机数func divByRand(numerator int) int { return numerator / int(rand.Intn(10))}我们如何测试这个函数呢?由于rand.Intn(10)是一个随机数,测试时没法通过一个输入值预测输出值,故无法测试。试想,如果我们能mock出随机数为一个固定数,那么就可以测试。但是上面的代码生成随机数的所有部分rand.Intn(10)都存于函数内部,我们无法mock。那怎么办呢?可以把随机数的生成部分抽象出来成一个接口,这个接口包含随机数的签名函数即可,优化代码如下:// mock_example/main.gopackage mainimport "math/rand"// 随机数生成器接口type randGenerator interface { randInt(max int) int}// 随机数生成器结构体type standardRand struct{}// 随机数生成器实现func (r *standardRand) randInt(max int) int { return rand.Intn(max)}// 除一个随机数 rg 随机数生成器接口func divByRand(rg randGenerator, numerator int) int { return numerator / int(1+rg.randInt(10))}func main() { // 创建一个随机数生成器 rg := &standardRand{} // 使用随机数生成器 divByRand(rg, 100)}现在我们可以对divByRand这个函数进行测试了。测试代码如下:// mock_example/main_test.gopackage mainimport ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock")type MockRandGenerator struct { mock.Mock}func (m *MockRandGenerator) randInt(max int) int { args := m.Called(max) return args.Int(0)}func TestDivByRand(t *testing.T) { mockRand := &MockRandGenerator{} mockRand.On("randInt", 10).Return(4) result := divByRand(mockRand, 10) assert.Equal(t, 2, result)}由此可见,非常重要的一步是,把依赖项抽象出来,形成接口,然后mock测试。这也是为什么go中可以看到大量的interface的原因之一,它便与测试。4. 一些技巧mock除了我们上面看到的on return形式外,还支持一些有意思的技巧。// 执行一次xxmock.On("randInt", 10).Return(4).Once()// 执行二次xxmock.On("randInt", 10).Return(5).Twice()// 执行3次xxmock.On("randInt", 10).Return(6).Times(3)// 可能被调用,也可能不被调用xxmock.On("randInt", 10).Return(6).Maybe()// 任意参数 返回固定值xxmock.On("MyMethod", mock.Anything).Return("固定返回值")04套件在go内置的testing包中,并没有提供一种将各种测试用例有效组织起来的方式;幸运的是testify为了我们提供了套件功能,它是支持的。1. 怎么用?套件怎么使用呢?其实也很简单:定义套件(suite)结构体嵌入(suite)定义套件结构体方法(测试/辅助)定义套件测试函数(引爆点,套件执行入口)直接看文字不太容易理解,我们还是直接看代码,假设我们要测试购物车相关的功能,代码如下:// main.gopackage main// 商品条目type Item struct { ID int Name string}// 购物车type ShoppingCart struct { Items []Item // 商品列表 Count int // 商品数量}// 添加购物车func (s *ShoppingCart) AddItem(item Item) { s.Items = append(s.Items, item) s.Count++}// 移除购物车func (s *ShoppingCart) RemoveItem(item Item) { for i, v := range s.Items { if v.ID == item.ID { s.Items = append(s.Items[:i], s.Items[i+1:]...) s.Count-- break } }}测试代码:// main_test.gopackage mainimport ( "testing" "github.com/stretchr/testify/suite")// =============step1: 定义套件结构体=================type ShoppingCartSuite struct { suite.Suite // 嵌入套件 cart *ShoppingCart // 购物车}// ============step2: 定义套件结构体的方法=============// 初始化套件(套件执行前)func (s *ShoppingCartSuite) SetupSuite() { s.T().Log("=====初始化套件=====") s.cart = &ShoppingCart{}}// 拆卸套件(套件执行后)func (s *ShoppingCartSuite) TearDownSuite() { s.T().Log("=====拆卸套件=====")}// 初始化测试(每个测试执行前 初始化工作)func (s *ShoppingCartSuite) SetupTest() { s.T().Log("=====初始化测试=====")}// 拆卸测试(每个测试执行后 清理工作)func (s *ShoppingCartSuite) TearDownTest() { s.T().Log("=====拆卸测试=====") s.cart.Count = 0 s.cart.Items = []Item{}}func (s *ShoppingCartSuite) TestAddItem() { s.T().Log("=====测试添加商品=====") s.cart.AddItem(Item{ID: 1, Name: "apple"}) s.cart.AddItem(Item{ID: 2, Name: "banana"}) s.cart.AddItem(Item{ID: 3, Name: "orange"}) s.Equal(3, s.cart.Count)}func (s *ShoppingCartSuite) TestRemoveItem() { s.T().Log("=====测试移除商品=====") s.cart.AddItem(Item{ID: 1, Name: "apple"}) s.cart.AddItem(Item{ID: 2, Name: "banana"}) s.cart.AddItem(Item{ID: 3, Name: "orange"}) s.cart.RemoveItem(Item{ID: 2, Name: "banana"}) s.Equal(2, s.cart.Count)}// step3: 测试套件引爆点(入口)func TestShoppingCart(t *testing.T) { // 通过suite.Run启动测试 suite.Run(t, new(ShoppingCartSuite))}运行测试go test -v=== RUN TestShoppingCart shopping_cart_test.go:18: =====初始化套件======== RUN TestShoppingCart/TestAddItem shopping_cart_test.go:29: =====初始化测试===== shopping_cart_test.go:40: =====测试添加商品===== shopping_cart_test.go:34: =====拆卸测试======== RUN TestShoppingCart/TestRemoveItem shopping_cart_test.go:29: =====初始化测试===== shopping_cart_test.go:48: =====测试移除商品===== shopping_cart_test.go:34: =====拆卸测试======== NAME TestShoppingCart shopping_cart_test.go:24: =====拆卸套件=====--- PASS: TestShoppingCart (0.00s) --- PASS: TestShoppingCart/TestAddItem (0.00s) --- PASS: TestShoppingCart/TestRemoveItem (0.00s)PASSok command-line-arguments 0.540s2. 层级关系从上面的例子我们可以看出,测试套件有很多层级的设定,可以在套件开始前、结束后、测试开始前、结束后执行某些操作,这在测试时非常用。比如:我们希望在测试套件开始前,创建一个数据库连接,测试结束后,关闭数据库连接;在某个测试开始前,清理上一个测试用例创建的数据等等。那么这些层级到底是怎样的?我们梳理下:SetupSuite # 套件开始前 SetupTest # 测试开始前(每个测试) BeforeTest(suiteName, testName) # 测试前 带测试名(每个测试) SetupSubTest() # 子测试开始前 TearDownSubTest() # 子测试结束后 AfterTest(suiteName, testName) # 测试后 带测试名(每个测试) TearDownTest # 测试结束后TearDownSuite # 套件结束后另外suite是支持子测试用法如下:func (suite *ExampleSuite) TestCase() { suite.T().Log("======TestCase=====") // 在函数执行后里继续运行就是子测试 suite.Run("case1-subtest1", func() { suite.T().Log("======TestCase.Subtest1=====") }) suite.Run("case1-subtest2", func() { suite.T().Log("======TestCase.Subtest2=====") })}

END链接:https://juejin.cn/post/7380200984059117594本文经授权转载,转载文章所包含的文字来源于作者。如因内容或版权等问题,请联系进行删除点击下方“阅读原文”,寻找职业新风向~

加微信咨询
小博老师 @博为峰
微信号:bwf******zx

提供专业的课程咨询服务

微信咨询
相关资讯
10月高薪通报 | 186人上榜,*高月薪20k!棒! *高月薪15.5k!干了3年运维后,他转行软件测试,3个月后薪资翻两番! 软件测试就是走流程,把软件试用一遍? 对不起,你的学历并没那么值钱! 想月入过万你就来!软件测试学习交流群,最新资料+项目实战+技术交流
相关课程