虽然Go testing包中提供了丰富的类型和⽅法,但是还是有⼀些⾮常有⽤的第三⽅⼯具,⽅便我们编写单元测试代码,这⾥我给⼤家介绍⼏个。
stretchr/testify
第⼀个不得不说的就是stretchr/testify,它提供了各种各样的断⾔。
前⾯我们说过 t.Fail
、 t.Fatal
可以标记单测失败或者停⽌执⾏。但是需要预先对期望值和实际的值进⾏⽐较,然后在调⽤ t.Fail
和 t.Fatal
标记失败。
testify可以⼤⼤简化相关的判断,如果你使⽤JUnit相关的⼯具,应该⽐较熟悉。
这个库实际有⼏个⼦package, 在平常使⽤中,mock库不太使⽤,因为我们有更好的mock可以⽤。suite包有点鸡肋,因为它和官⽅的概念不相匹配,⾃定定义出Suite的概念。最常⽤的是asset包和reuire包。
assert类似Fail,断⾔失败不会导致此测试停⽌,后续的逻辑还是会继续执⾏。
require类似Fatal,⼀旦测试失败,此测试就会结束。
两个的断⾔⽅法都是类似的。
啥事断⾔呢?看下⾯的例⼦:
package yours
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
// 断⾔两个值相等,否则就失败,并输出提示⽂本
assert.Equal(t, 123, 123, "they should be equal")
// 断⾔不相等
assert.NotEqual(t, 123, 456, "they should not be equal")
// 断⾔object是nil值
assert.Nil(t, object)
// 断⾔object⾮nil
if assert.NotNil(t, object) {
// 断⾔object.Value和某值相等
assert.Equal(t, "Something", object.Value)
}
}
这⾥assert也可以换成require.如果换成require,⼀旦遇到某个测试没通过,那么后续的测试就不再执⾏了。
什么时候⽤assert什么时候⽤require? 在处理必要的测试结果⽤require,⽐如数据库的连接,如果连接没有建⽴,那么后续的测试也没有必要测试了,所以此时适合⽤require。
这个库打磨的真的⾮常的强⼤了,在本书中我就不把它的所有⽅法复制过来了,推荐你看看它的testify@v1.8.0/assert"target="_blank">go doc ⽂档,⼏⼗个⽅法提供了丰富的断⾔。
举个例⼦,你可以使⽤ Error
、 NoError
判断error
是否为nil
。你可以判断两个值是否相等,⼤于或者⼩于。你可以判断⽂件和⽂件夹是否存在,HTTP的结果的判断。集合是否是增序还是降序,集合是否包含某值,集合是否为空。判断JSON值是否相等 assert.JSONEq(t, {"hello":"world", "foo": "bar"} , {"foo": "bar", "hello": "world"} )
。
google/go-cmp
如果你使⽤go标准库的⽅法,判断两个值是否相等并显示值的不同时,如果值的类似是复杂的struct类型,你并不容易把两个值的不同区分出来并显示。google/go-cmp可以提供给你帮助。
你可以使⽤它打印两个struct的不同。
⽐如下⾯的例⼦:
package main
import (
"fmt"
"net"
"time"
"github.com/google/go-cmp/cmp"
)
func main() {
// Let got be the hypothetical value obtained from some logic under test
// and want be the expected golden data.
got, want := MakeGatewayInfo()
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff)
}
}
type (
Gateway struct {
SSID string
IPAddress net.IP
NetMask net.IPMask
Clients []Client
}
Client struct {
Hostname string
IPAddress net.IP
LastSeen time.Time
}
)
func MakeGatewayInfo() (x, y Gateway) {
x = Gateway{
SSID: "CoffeeShopWiFi",
IPAddress: net.IPv4(192, 168, 0, 1),
NetMask: net.IPv4Mask(255, 255, 0, 0),
Clients: []Client{{
Hostname: "ristretto",
IPAddress: net.IPv4(192, 168, 0, 116),
}, {
Hostname: "aribica",
IPAddress: net.IPv4(192, 168, 0, 104),
LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0,
time.UTC),
}, {
Hostname: "macchiato",
IPAddress: net.IPv4(192, 168, 0, 153),
LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0,
time.UTC),
}, {
Hostname: "espresso",
IPAddress: net.IPv4(192, 168, 0, 121),
}, {
Hostname: "latte",
IPAddress: net.IPv4(192, 168, 0, 219),
LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0,
time.UTC),
}, {
Hostname: "americano",
IPAddress: net.IPv4(192, 168, 0, 188),
LastSeen: time.Date(2009, time.November, 10, 23, 3, 5, 0,
time.UTC),
}},
}
y = Gateway{
SSID: "CoffeeShopWiFi",
IPAddress: net.IPv4(192, 168, 0, 2),
NetMask: net.IPv4Mask(255, 255, 0, 0),
Clients: []Client{{
Hostname: "ristretto",
IPAddress: net.IPv4(192, 168, 0, 116),
}, {
Hostname: "aribica",
IPAddress: net.IPv4(192, 168, 0, 104),
LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0,
time.UTC),
}, {
Hostname: "macchiato",
IPAddress: net.IPv4(192, 168, 0, 153),
LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0,
time.UTC),
}, {
Hostname: "espresso",
IPAddress: net.IPv4(192, 168, 0, 121),
}, {
Hostname: "latte",
IPAddress: net.IPv4(192, 168, 0, 221),
LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0,
time.UTC),
}},
}
return x, y
}
var t fakeT
type fakeT struct{}
func (t fakeT) Errorf(format string, args ...interface{}) {
fmt.Printf(format+"\n", args...) }
它的输出结果是:
MakeGatewayInfo() mismatch (-want +got):
cmp_test.Gateway{
SSID: "CoffeeShopWiFi",
- IPAddress: s"192.168.0.2",
+ IPAddress: s"192.168.0.1",
NetMask: s"ffff0000",
Clients: []cmp_test.Client{
... // 2 identical elements
{Hostname: "macchiato", IPAddress: s"192.168.0.153", LastSeen:s"2009-11-10 23:39:43 +0000 UTC"},
{Hostname: "espresso", IPAddress: s"192.168.0.121"},
{
Hostname: "latte",
- IPAddress: s"192.168.0.221",
+ IPAddress: s"192.168.0.219",
LastSeen: s"2009-11-10 23:00:23 +0000 UTC",
},
+ {
+ Hostname: "americano",
+ IPAddress: s"192.168.0.188",
+ LastSeen: s"2009-11-10 23:03:05 +0000 UTC",
+ },
},
}
gocovery
smartystreets/goconvey是另外⼀种单元测试⻛格。这是⼀种称之为⾏为驱动开发(Behaviordriven Development)的敏捷开发⽅式。
BDD的重点是通过与利益相关者的讨论取得对预期的软件⾏为的清醒认识。它通过⽤⾃然语⾔书写⾮程序员可读的测试⽤例扩展了测试驱动开发⽅法。⾏为驱动开发⼈员使⽤混合了领域中统⼀的语⾔的⺟语语⾔来描述他们的代码的⽬的。这让开发者得以把精⼒集中在代码应该怎么写,⽽不是技术细节上,⽽且也最⼤程度的减少了将代码编写者的技术语⾔与商业客户、⽤户、利益相关者、项⽬管理者等的领域语⾔之间来回翻译的代价。
goconvey不仅提供了相应的辅助编写BDD⻛格的库,还提供了⼯具,在浏览器中展示测试的结果。
下⾯是⼀个使⽤goconvey编写的单元测试的例⼦:
package s5
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestGocovery(t *testing.T) {
Convey("Given a starting integer value", t, func() {
x := 42
Convey("When incremented", func() {
x++
Convey("The value should be greater by one", func() {
So(x, ShouldEqual, 43)
})
Convey("The value should NOT be what it used to be", func() {
So(x, ShouldNotEqual, 42)
})
})
Convey("When decremented", func() {
x--
Convey("The value should be lesser by one", func() {
So(x, ShouldEqual, 41)
})
Convey("The value should NOT be what it used to be", func() {
So(x, ShouldNotEqual, 42)
})
})
})
}
Convey 在声明⼀个规范的作⽤域时使⽤。每个作⽤域有⼀个⽂本描述信息和⼀个函数,此函数体内可以调⽤其它的Convey,Reset或者Should⻛格的断⾔。
如你所⻅Convery可以嵌套。嵌套时最顶层的Convey的函数签名是 Convey(description string, t *testing.T, action func())
,⽽其它的Convey则不需要传递 t :Convey(description string, action func())
或者是 Convey(description string, actionfunc(c C))
。
纵观开源的Go项⽬和 Go标准库, goconvey这种测试⻛格使⽤者还是少数。
最后编辑:admin 更新时间:2024-10-22 02:08