Go语言测试库testify使用学习
怎么入门Golang编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《Go语言测试库testify使用学习》,涉及到测试库、testify,有需要的可以收藏一下
简介
testify可以说是最流行的(从 GitHub star 数来看)Go 语言测试库了。testify提供了很多方便的函数帮助我们做assert和错误信息输出。使用标准库testing,我们需要自己编写各种条件判断,根据判断结果决定输出对应的信息。
testify核心有三部分内容:
assert:断言;mock:测试替身;suite:测试套件。
准备工作
本文代码使用 Go Modules。
创建目录并初始化:
$ mkdir -p testify && cd testify $ go mod init github.com/darjun/go-daily-lib/testify
安装testify库:
$ go get -u github.com/stretchr/testify
assert
assert子库提供了便捷的断言函数,可以大大简化测试代码的编写。总的来说,它将之前需要判断 + 信息输出的模式:
if got != expected {
t.Errorf("Xxx failed expect:%d got:%d", got, expected)
}
简化为一行断言代码:
assert.Equal(t, got, expected, "they should be equal")
结构更清晰,更可读。熟悉其他语言测试框架的开发者对assert的相关用法应该不会陌生。此外,assert中的函数会自动生成比较清晰的错误描述信息:
func TestEqual(t *testing.T) {
var a = 100
var b = 200
assert.Equal(t, a, b, "")
}
使用testify编写测试代码与testing一样,测试文件为_test.go,测试函数为TestXxx。使用go test命令运行测试:
$ go test
--- FAIL: TestEqual (0.00s)
assert_test.go:12:
Error Trace:
Error: Not equal:
expected: 100
actual : 200
Test: TestEqual
FAIL
exit status 1
FAIL github.com/darjun/go-daily-lib/testify/assert 0.107s
我们看到信息更易读。
testify提供的assert类函数众多,每种函数都有两个版本,一个版本是函数名不带f的,一个版本是带f的,区别就在于带f的函数,我们需要指定至少两个参数,一个格式化字符串format,若干个参数args:
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{})
func Equalf(t TestingT, expected, actual interface{}, msg string, args ...interface{})
实际上,在Equalf()函数内部调用了Equal():
func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return Equal(t, expected, actual, append([]interface{}{msg}, args...)...)
}
所以,我们只需要关注不带f的版本即可。
Contains
函数类型:
func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool
Contains断言s包含contains。其中s可以是字符串,数组/切片,map。相应地,contains为子串,数组/切片元素,map 的键。
DirExists
函数类型:
func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool
DirExists断言路径path是一个目录,如果path不存在或者是一个文件,断言失败。
ElementsMatch
函数类型:
func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) bool
ElementsMatch断言listA和listB包含相同的元素,忽略元素出现的顺序。listA/listB必须是数组或切片。如果有重复元素,重复元素出现的次数也必须相等。
Empty
函数类型:
func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
Empty断言object是空,根据object中存储的实际类型,空的含义不同:
- 指针:
nil; - 整数:0;
- 浮点数:0.0;
- 字符串:空串
""; - 布尔:false;
- 切片或 channel:长度为 0。
EqualError
函数类型:
func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool
EqualError断言theError.Error()的返回值与errString相等。
EqualValues
函数类型:
func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
EqualValues断言expected与actual相等,或者可以转换为相同的类型,并且相等。这个条件比Equal更宽,Equal()返回true则EqualValues()肯定也返回true,反之则不然。实现的核心是下面两个函数,使用了reflect.DeapEqual():
func ObjectsAreEqual(expected, actual interface{}) bool {
if expected == nil || actual == nil {
return expected == actual
}
exp, ok := expected.([]byte)
if !ok {
return reflect.DeepEqual(expected, actual)
}
act, ok := actual.([]byte)
if !ok {
return false
}
if exp == nil || act == nil {
return exp == nil && act == nil
}
return bytes.Equal(exp, act)
}
func ObjectsAreEqualValues(expected, actual interface{}) bool {
// 如果`ObjectsAreEqual`返回 true,直接返回
if ObjectsAreEqual(expected, actual) {
return true
}
actualType := reflect.TypeOf(actual)
if actualType == nil {
return false
}
expectedValue := reflect.ValueOf(expected)
if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
// 尝试类型转换
return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual)
}
return false
}
例如我基于int定义了一个新类型MyInt,它们的值都是 100,Equal()调用将返回 false,EqualValues()会返回 true:
type MyInt int
func TestEqual(t *testing.T) {
var a = 100
var b MyInt = 100
assert.Equal(t, a, b, "")
assert.EqualValues(t, a, b, "")
}
Error
函数类型:
func Error(t TestingT, err error, msgAndArgs ...interface{}) bool
Error断言err不为nil。
ErrorAs
函数类型:
func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool
ErrorAs断言err表示的 error 链中至少有一个和target匹配。这个函数是对标准库中errors.As的包装。
ErrorIs
函数类型:
func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool
ErrorIs断言err的 error 链中有target。
逆断言
上面的断言都是它们的逆断言,例如NotEqual/NotEqualValues等。
Assertions 对象
观察到上面的断言都是以TestingT为第一个参数,需要大量使用时比较麻烦。testify提供了一种方便的方式。先以*testing.T创建一个*Assertions对象,Assertions定义了前面所有的断言方法,只是不需要再传入TestingT参数了。
func TestEqual(t *testing.T) {
assertions := assert.New(t)
assertion.Equal(a, b, "")
// ...
}
顺带提一句TestingT是一个接口,对*testing.T做了一个简单的包装:
type TestingT interface{
Errorf(format string, args ...interface{})
}
require
require提供了和assert同样的接口,但是遇到错误时,require直接终止测试,而assert返回false。
mock
testify提供了对 Mock 的简单支持。Mock 简单来说就是构造一个仿对象,仿对象提供和原对象一样的接口,在测试中用仿对象来替换原对象。这样我们可以在原对象很难构造,特别是涉及外部资源(数据库,访问网络等)。例如,我们现在要编写一个从一个站点拉取用户列表信息的程序,拉取完成之后程序显示和分析。如果每次都去访问网络会带来极大的不确定性,甚至每次返回不同的列表,这就给测试带来了极大的困难。我们可以使用 Mock 技术。
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type User struct {
Name string
Age int
}
type ICrawler interface {
GetUserList() ([]*User, error)
}
type MyCrawler struct {
url string
}
func (c *MyCrawler) GetUserList() ([]*User, error) {
resp, err := http.Get(c.url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var userList []*User
err = json.Unmarshal(data, &userList)
if err != nil {
return nil, err
}
return userList, nil
}
func GetAndPrintUsers(crawler ICrawler) {
users, err := crawler.GetUserList()
if err != nil {
return
}
for _, u := range users {
fmt.Println(u)
}
}
Crawler.GetUserList()方法完成爬取和解析操作,返回用户列表。为了方便 Mock,GetAndPrintUsers()函数接受一个ICrawler接口。现在来定义我们的 Mock 对象,实现ICrawler接口:
package main
import (
"github.com/stretchr/testify/mock"
"testing"
)
type MockCrawler struct {
mock.Mock
}
func (m *MockCrawler) GetUserList() ([]*User, error) {
args := m.Called()
return args.Get(0).([]*User), args.Error(1)
}
var (
MockUsers []*User
)
func init() {
MockUsers = append(MockUsers, &User{"dj", 18})
MockUsers = append(MockUsers, &User{"zhangsan", 20})
}
func TestGetUserList(t *testing.T) {
crawler := new(MockCrawler)
crawler.On("GetUserList").Return(MockUsers, nil)
GetAndPrintUsers(crawler)
crawler.AssertExpectations(t)
}
实现GetUserList()方法时,需要调用Mock.Called()方法,传入参数(示例中无参数)。Called()会返回一个mock.Arguments对象,该对象中保存着返回的值。它提供了对基本类型和error的获取方法Int()/String()/Bool()/Error(),和通用的获取方法Get(),通用方法返回interface{},需要类型断言为具体类型,它们都接受一个表示索引的参数。
crawler.On("GetUserList").Return(MockUsers, nil)是 Mock 发挥魔法的地方,这里指示调用GetUserList()方法的返回值分别为MockUsers和nil,返回值在上面的GetUserList()方法中被Arguments.Get(0)和Arguments.Error(1)获取。
最后crawler.AssertExpectations(t)对 Mock 对象做断言。
运行:
$ go test
&{dj 18}
&{zhangsan 20}
PASS
ok github.com/darjun/testify 0.258s
GetAndPrintUsers()函数功能正常执行,并且我们通过 Mock 提供的用户列表也能正确获取。
使用 Mock,我们可以精确断言某方法以特定参数的调用次数,Times(n int),它有两个便捷函数Once()/Twice()。下面我们要求函数Hello(n int)要以参数 1 调用 1次,参数 2 调用两次,参数 3 调用 3 次:
type IExample interface {
Hello(n int) int
}
type Example struct {
}
func (e *Example) Hello(n int) int {
fmt.Printf("Hello with %d\n", n)
return n
}
func ExampleFunc(e IExample) {
for n := 1; n <= 3; n++ {
for i := 0; i <= n; i++ {
e.Hello(n)
}
}
}
编写 Mock 对象:
type MockExample struct {
mock.Mock
}
func (e *MockExample) Hello(n int) int {
args := e.Mock.Called(n)
return args.Int(0)
}
func TestExample(t *testing.T) {
e := new(MockExample)
e.On("Hello", 1).Return(1).Times(1)
e.On("Hello", 2).Return(2).Times(2)
e.On("Hello", 3).Return(3).Times(3)
ExampleFunc(e)
e.AssertExpectations(t)
}
运行:
$ go test
--- FAIL: TestExample (0.00s)
panic:
assert: mock: The method has been called over 1 times.
Either do one more Mock.On("Hello").Return(...), or remove extra call.
This call was unexpected:
Hello(int)
0: 1
at: [equal_test.go:13 main.go:22] [recovered]
原来ExampleFunc()函数中应该是导致多调用了一次,修改过来继续运行:
$ go test PASS ok github.com/darjun/testify 0.236s
我们还可以设置以指定参数调用会导致 panic,测试程序的健壮性:
e.On("Hello", 100).Panic("out of range")
suite
testify提供了测试套件的功能(TestSuite),testify测试套件只是一个结构体,内嵌一个匿名的suite.Suite结构。测试套件中可以包含多个测试,它们可以共享状态,还可以定义钩子方法执行初始化和清理操作。钩子都是通过接口来定义的,实现了这些接口的测试套件结构在运行到指定节点时会调用对应的方法。
type SetupAllSuite interface {
SetupSuite()
}
如果定义了SetupSuite()方法(即实现了SetupAllSuite接口),在套件中所有测试开始运行前调用这个方法。对应的是TearDownAllSuite:
type TearDownAllSuite interface {
TearDownSuite()
}
如果定义了TearDonwSuite()方法(即实现了TearDownSuite接口),在套件中所有测试运行完成后调用这个方法。
type SetupTestSuite interface {
SetupTest()
}
如果定义了SetupTest()方法(即实现了SetupTestSuite接口),在套件中每个测试执行前都会调用这个方法。对应的是TearDownTestSuite:
type TearDownTestSuite interface {
TearDownTest()
}
如果定义了TearDownTest()方法(即实现了TearDownTest接口),在套件中每个测试执行后都会调用这个方法。
还有一对接口BeforeTest/AfterTest,它们分别在每个测试运行前/后调用,接受套件名和测试名作为参数。
我们来编写一个测试套件结构作为演示:
type MyTestSuit struct {
suite.Suite
testCount uint32
}
func (s *MyTestSuit) SetupSuite() {
fmt.Println("SetupSuite")
}
func (s *MyTestSuit) TearDownSuite() {
fmt.Println("TearDownSuite")
}
func (s *MyTestSuit) SetupTest() {
fmt.Printf("SetupTest test count:%d\n", s.testCount)
}
func (s *MyTestSuit) TearDownTest() {
s.testCount++
fmt.Printf("TearDownTest test count:%d\n", s.testCount)
}
func (s *MyTestSuit) BeforeTest(suiteName, testName string) {
fmt.Printf("BeforeTest suite:%s test:%s\n", suiteName, testName)
}
func (s *MyTestSuit) AfterTest(suiteName, testName string) {
fmt.Printf("AfterTest suite:%s test:%s\n", suiteName, testName)
}
func (s *MyTestSuit) TestExample() {
fmt.Println("TestExample")
}
这里只是简单在各个钩子函数中打印信息,统计执行完成的测试数量。由于要借助go test运行,所以需要编写一个TestXxx函数,在该函数中调用suite.Run()运行测试套件:
func TestExample(t *testing.T) {
suite.Run(t, new(MyTestSuit))
}
suite.Run(t, new(MyTestSuit))会将运行MyTestSuit中所有名为TestXxx的方法。运行:
$ go test SetupSuite SetupTest test count:0 BeforeTest suite:MyTestSuit test:TestExample TestExample AfterTest suite:MyTestSuit test:TestExample TearDownTest test count:1 TearDownSuite PASS ok github.com/darjun/testify 0.375s
测试 HTTP 服务器
Go 标准库提供了一个httptest用于测试 HTTP 服务器。现在编写一个简单的 HTTP 服务器:
func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World")
}
func greeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "welcome, %s", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", index)
mux.HandleFunc("/greeting", greeting)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
很简单。httptest提供了一个ResponseRecorder类型,它实现了http.ResponseWriter接口,但是它只是记录写入的状态码和响应内容,不会发送响应给客户端。这样我们可以将该类型的对象传给处理器函数。然后构造服务器,传入该对象来驱动请求处理流程,最后测试该对象中记录的信息是否正确:
func TestIndex(t *testing.T) {
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil)
mux := http.NewServeMux()
mux.HandleFunc("/", index)
mux.HandleFunc("/greeting", greeting)
mux.ServeHTTP(recorder, request)
assert.Equal(t, recorder.Code, 200, "get index error")
assert.Contains(t, recorder.Body.String(), "Hello World", "body error")
}
func TestGreeting(t *testing.T) {
recorder := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/greeting", nil)
request.URL.RawQuery = "name=dj"
mux := http.NewServeMux()
mux.HandleFunc("/", index)
mux.HandleFunc("/greeting", greeting)
mux.ServeHTTP(recorder, request)
assert.Equal(t, recorder.Code, 200, "greeting error")
assert.Contains(t, recorder.Body.String(), "welcome, dj", "body error")
}
运行:
$ go test PASS ok github.com/darjun/go-daily-lib/testify/httptest 0.093s
很简单,没有问题。
但是我们发现一个问题,上面的很多代码有重复,recorder/mux等对象的创建,处理器函数的注册。使用suite我们可以集中创建,省略这些重复的代码:
type MySuite struct {
suite.Suite
recorder *httptest.ResponseRecorder
mux *http.ServeMux
}
func (s *MySuite) SetupSuite() {
s.recorder = httptest.NewRecorder()
s.mux = http.NewServeMux()
s.mux.HandleFunc("/", index)
s.mux.HandleFunc("/greeting", greeting)
}
func (s *MySuite) TestIndex() {
request, _ := http.NewRequest("GET", "/", nil)
s.mux.ServeHTTP(s.recorder, request)
s.Assert().Equal(s.recorder.Code, 200, "get index error")
s.Assert().Contains(s.recorder.Body.String(), "Hello World", "body error")
}
func (s *MySuite) TestGreeting() {
request, _ := http.NewRequest("GET", "/greeting", nil)
request.URL.RawQuery = "name=dj"
s.mux.ServeHTTP(s.recorder, request)
s.Assert().Equal(s.recorder.Code, 200, "greeting error")
s.Assert().Contains(s.recorder.Body.String(), "welcome, dj", "body error")
}
最后编写一个TestXxx驱动测试:
func TestHTTP(t *testing.T) {
suite.Run(t, new(MySuite))
}
总结
testify扩展了testing标准库,断言库assert,测试替身mock和测试套件suite,让我们编写测试代码更容易!
参考
- testify GitHub:github.com/stretchr/testify
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
终于介绍完啦!小伙伴们,这篇关于《Go语言测试库testify使用学习》的介绍应该让你收获多多了吧!欢迎大家收藏或分享给更多需要学习的朋友吧~golang学习网公众号也会发布Golang相关知识,快来关注吧!
Golang实现Biginteger大数计算实例详解
- 上一篇
- Golang实现Biginteger大数计算实例详解
- 下一篇
- Go1.16新特性embed打包静态资源文件实现
-
- Golang · Go教程 | 8分钟前 |
- GolangWebAPI设计与错误处理方法
- 490浏览 收藏
-
- Golang · Go教程 | 25分钟前 | golang 日志优化
- Golang日志优化技巧分享
- 428浏览 收藏
-
- Golang · Go教程 | 35分钟前 |
- VSCode配置Go插件及自动补全教程
- 228浏览 收藏
-
- Golang · Go教程 | 44分钟前 |
- Golang变量的零值是什么?
- 342浏览 收藏
-
- Golang · Go教程 | 48分钟前 |
- Golang大文件读取优化技巧分享
- 136浏览 收藏
-
- Golang · Go教程 | 50分钟前 |
- Golang性能测试技巧与常见陷阱
- 107浏览 收藏
-
- Golang · Go教程 | 52分钟前 | Golang并发 缓存更新 sync.RWMutex sync/atomic bigcache
- Golang并发缓存更新方法与技巧
- 446浏览 收藏
-
- Golang · Go教程 | 52分钟前 |
- GolangHTTP连接复用优化方法
- 264浏览 收藏
-
- Golang · Go教程 | 57分钟前 |
- Golang指针与闭包变量捕获详解
- 279浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Golang环境配置教程(Linux版)
- 391浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Golang代理模式与权限控制结合应用
- 377浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go语言Windows下Linux交叉编译教程
- 389浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3178次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3389次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3418次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4523次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3797次使用
-
- Golangmap实践及实现原理解析
- 2022-12-28 505浏览
-
- go和golang的区别解析:帮你选择合适的编程语言
- 2023-12-29 503浏览
-
- 试了下Golang实现try catch的方法
- 2022-12-27 502浏览
-
- 如何在go语言中实现高并发的服务器架构
- 2023-08-27 502浏览
-
- 提升工作效率的Go语言项目开发经验分享
- 2023-11-03 502浏览

