从入门到深入 Go 我们已经走了很长的路,当你想启动多个测试类的时候你是不是想启动多个 main 方法,但是 Go 限制了在同一个 package 下只能有一个 main,所以这条路你是走不通的。那我们想写单元测试的时候应该如何操作呢?别着急,不用引入任何的第三方包,单元测试 Go 也有默认的规范写法。

约定

在 Go SDK 中 ”testing“ 包的内容就是 Go 默认提供的单元测试支持。Go 标准库对单元测试编写的格式有一些硬性要求:

  • 所有测试方法必须放在位于以 _test.go 结尾的文件中,这样在执行 go build 构建的时候测试代码才会被排除。
  • 测试函数的命名必须以 Test 开头,并且跟在 Test 后面的后缀开头第一个字母必须大写
  • 测试方法必须要包含 “t *testing.T” 参数。
func TestGetUser(t *testing.T)
func TestInsert(t *testing.T)

其中参数 t 用于报告测试失败和附加的日志信息。 testing.T 的拥有的方法如下:

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool

比如我们现在有一段回文检测的 func:

package service

// 判断一个字符串s是否时回文字符串
func IsPalindrome(s string) bool {
for i := range s {
if s[i] != s[len(s)-1-i] {
return false
}
}
return true
}

想在单元测试中调用 这个方法:

package demo

import (
"gorm-demo/service"
"testing"
) func TestString(t *testing.T) {
palindrome := service.IsPalindrome("3ee3")
if palindrome {
t.Logf("IsPalindrome test success, param=%s", "3e1e3")
} else {
t.Fatalf("IsPalindrome test fail, param=%s", "3e1e3")
} }

根据是否是回文输出对应的结果:

=== RUN   TestString
string_test.go:11: IsPalindrome test success, param=3e1e3
--- PASS: TestString (0.00s)
PASS

除了直接执行对应的测试方法之外我们还可以通过 go test 命令行的方式来执行测试,go test 是 Go 语言自带的测试工具,其中包含的是两类:单元测试和性能测试,通过 go help test 可以看到 go test 的使用说明:

格式形如: go test [-c] [-i] [build flags] [packages] [flags for test binary]

参数解读:

-c : 编译go test成为可执行的二进制文件,但是不运行测试。

-i : 安装测试包依赖的package,但是不运行测试。

关于build flags,调用go help build,这些是编译运行过程中需要使用到的参数,一般设置为空

关于packages,调用go help packages,这些是关于包的管理,一般设置为空

关于flags for test binary,调用go help testflag,这些是go test过程中经常使用到的参数

-test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。

-test.run pattern: 只跑哪些单元测试用例

-test.bench patten: 只跑那些性能测试用例

-test.benchmem : 是否在性能测试的时候输出内存情况

-test.benchtime t : 性能测试运行的时间,默认是1s

-test.cpuprofile cpu.out : 是否输出cpu性能分析文件

-test.memprofile mem.out : 是否输出内存性能分析文件

-test.blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件

-test.memprofilerate n : 内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打点的内存分配间隔,也就是profile中一个sample代表的内存大小。默认是设置为512 * 1024的。如果你将它设置为1,则每分配一个内存块就会在profile中有个打点,那么生成的profile的sample就会非常多。如果你设置为0,那就是不做打点了。

你可以通过设置memprofilerate=1和GOGC=off来关闭内存回收,并且对每个内存块的分配进行观察。

-test.blockprofilerate n: 基本同上,控制的是goroutine阻塞时候打点的纳秒数。默认不设置就相当于-test.blockprofilerate=1,每一纳秒都打点记录一下

-test.parallel n : 性能测试的程序并行cpu数,默认等于GOMAXPROCS。

-test.timeout t : 如果测试用例运行时间超过t,则抛出panic

-test.cpu 1,2,4 : 程序运行在哪些CPU上面,使用二进制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一个道理

-test.short : 将那些运行时间较长的测试用例运行时间缩短

测试覆盖率

Go提供内置功能来检查你的代码覆盖率。我们可以使用 go test -cover 来查看测试覆盖率。

MacBook-Pro:mockDemo yy$ go test -cover
PASS
coverage: 0.0% of statements
ok gorm-demo/test/mockDemo 0.007s

Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

MacBook-Pro:mockDemo yy$ go test -cover -coverprofile=tt.log
PASS
coverage: 0.0% of statements
ok gorm-demo/test/mockDemo 0.007s

生成 tt.log 文件之后,执行 go tool cover -html=tt.log,使用 cover 工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个 HTML 报告。

断言

使用 Java 的同学看到这里估计会问: Go 中没有断言吗?还需要自己去判断。

其实没有断言这种东西我们仔细想想也并不难理解,从 Go 的 error 包设计将异常作为返回值而不是使用 try-catch 的模式来说,Go 希望你在测试阶段就知晓每一个可能出现的异常,而不是将异常吞掉。所以 Assert 这种吞掉错误的功能 Go 官方也不想提供。

当然 Go 官方不提供不代表广大开发同胞真的不想用,这不有大哥开发了灵活又好用的断言库 testify ,有了它,我们上面的代码就可以改为这样:

assert.True(t, service.IsPalindrome("3e45e3"))

输出:

=== RUN   TestString
string_test.go:11:
Error Trace: string_test.go:11
Error: Should be true
Test: TestString
--- FAIL: TestString (0.00s)
FAIL

简介明了,一眼就知道测试用例是否通过。真的是谁用谁知道。

具体 testify 还有很多实用的断言方法:

// 判断两个值是否相等
func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
// 判断两个值不相等
func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
// 测试失败,测试中断
func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool
// 判断值是否为nil,常用于 error 的判断
func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
// 判断值是否不为nil,常用于 error 的判断
func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool

大家有兴趣可以看看 API。

Mock 功能

使用这个功能之前,先着重声明 mock 的意思。

mock 模拟,模仿的意思。这里这里提供的功能是模拟某段功能,用我们的模拟逻辑去代替。

testify 也支持 Mock,不过 Go 原生的 mock 框架就挺好的。GoMock 是由 Go 官方开发维护的测试框架,实现了较为完整的基于 interface 的 Mock 功能。注意它没在 SDK 里面哈。

go get -u github.com/golang/mock/gomock

Gomock 还提供了 mockgen 工具用来辅助生成测试代码。

go get -u github.com/golang/mock/mockgen

使用的时候这两个包都需要安装。

安装 mockgen 有两种方式,你可以只在你的当前代码目录执行 go get ,这样 mockgen 命令只对当前目录有效;或者你直接取 mockgen 的目录下执行 go build ,编译后会在这个目录下生成一个可执行程序 mockgen。然后将这个可执行程序 mockgen 拖到 $GOPATH/bin/ 目录下后面你就可以全局使用 mockgen 。

mockgen 使用也很简单,可以对包或者源代码文件生成指定接口的 Mock 代码,注意是对接口文件哈。

package mockDemo

type Task interface {
Do(string) (bool, error)
}

想对指定接口生成 mock 代码使用如下命令:

mockgen -source=源文件路径  -destination=写入文件的路径(没有这个参数输出到终端) -package=生成文件的包名

demo :
mockgen -source=/Users/cc/go/src/go-web-demo/test/mockDemo/task.go -destination=/Users/cc/go/src/go-web-demo/test/mockDemo/mock_task_test.go -package=mockDemo -source:设置需要模拟(mock)的接口文件
-destination:设置 mock 文件输出的地方,若不设置则打印到标准输出中
-package:设置 mock 文件的包名,若不设置则为 `mock_` 前缀加上文件名(如本文的包名会为 mock_person)

接下来上示例,再次解释 mock 就是要模拟,比如我们的 Do 方法要去连接数据库查询数据,这里因为不方便测试连接数据库这段代码,但是又不想影响整体测试流程所以用 mock 的方式去替代这段逻辑。解释清楚了我们上代码。

整体测试代码如下:

接口:

package mockDemo

type Task interface {
Do(string) (bool, error)
}

根据该接口生成 mock 类:

// Code generated by MockGen. DO NOT EDIT.
// Source: /Users/yangyue/go/src/go-web-demo/test/mockDemo/task.go // Package mockDemo is a generated GoMock package.
package mockDemo import (
reflect "reflect" gomock "github.com/golang/mock/gomock"
) // MockTask is a mock of Task interface.
type MockTask struct {
ctrl *gomock.Controller
recorder *MockTaskMockRecorder
} // MockTaskMockRecorder is the mock recorder for MockTask.
type MockTaskMockRecorder struct {
mock *MockTask
} // NewMockTask creates a new mock instance.
func NewMockTask(ctrl *gomock.Controller) *MockTask {
mock := &MockTask{ctrl: ctrl}
mock.recorder = &MockTaskMockRecorder{mock}
return mock
} // EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTask) EXPECT() *MockTaskMockRecorder {
return m.recorder
} // Do mocks base method.
func (m *MockTask) Do(arg0 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", arg0)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
} // Do indicates an expected call of Do.
func (mr *MockTaskMockRecorder) Do(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockTask)(nil).Do), arg0)
}

测试方法:

package mockDemo

import (
"fmt"
"github.com/golang/mock/gomock"
"testing"
) func TestMock(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
task := NewMockTask(ctl)
gomock.InOrder(task.EXPECT().Do("banana").Return(true, nil)) task.Do("banana")
}

gomock.NewController:返回 gomock.Controller,它代表 mock 生态系统中的顶级控件。定义了 mock 对象的范围、生命周期和期待值。多 goroutine 下是线程安全的。

NewMockTask() 创建一个新的 MockTask 实例,因为 MockTask 实现了 Task 接口所有后面实际是调用 MockTask 的实现方法。

gomock.InOrder(calls ...*Call):声明调用 Call 的顺序,这里可以传入多个 Call。

task.EXPECT().Do("banana").Return(true, nil)EXPECT() 是期望拿到返回值,Call 的方法调用类似于 Java 中的 Build 模式,链式调用。有如下方法可供使用:

  • Call.Do():声明在匹配时要运行的操作
  • Call.DoAndReturn():声明在匹配调用时要运行的操作,并且模拟返回该函* 数的返回值
  • Call.MaxTimes():设置最大的调用次数为 n 次
  • Call.MinTimes():设置最小的调用次数为 n 次
  • Call.AnyTimes():允许调用次数为 0 次或更多次
  • Call.Times():设置调用次数为 n 次

我们测试一下调用顺序检测,多个 Call 的情况:

package mockDemo

import (
"github.com/golang/mock/gomock"
"testing"
) func TestMock(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
task := NewMockTask(ctl) call1 := task.EXPECT().Do("banana").Return(true, nil)
call2 := task.EXPECT().Do("apple").Return(true, nil)
call3 := task.EXPECT().Do("pineapple").Return(true, nil) gomock.InOrder(call1, call2, call3) task.Do("apple")
task.Do("banana")
task.Do("pineapple")
}

顺序不一样的情况下是会报错的。

总结一下 mock 的使用:mock 是面向接口的测试,当你想测试的逻辑只是一段独立功能性的代码而没有提供接口去抽象化的时候你无法使用 mock 功能。当然不是说必须要面向接口开发,有接口的定义会更加规范化你的代码让你知道写出来的逻辑是审慎总结的。

跟我一起学Go系列:从写测试用例开始仗剑走天涯的更多相关文章

  1. [Android开发学iOS系列] iOS写UI的几种方式

    [Android开发学iOS系列] iOS写UI的几种方式 作为一个现代化的平台, iOS的发展也经历了好几个时代. 本文讲讲iOS写UI的几种主要方式和各自的特点. iOS写UI的方式 在iOS中写 ...

  2. .net基础学java系列(四)Console实操

    上一篇文章 .net基础学java系列(三)徘徊反思 本章节没啥营养,请绕路! 看视频,不实操,对于上了年龄的人来说,是记不住的!我已经看了几遍IDEA的教学视频: https://edu.51cto ...

  3. .net基础学java系列(二)IDE 之 插件

    上一篇文章.net基础学java系列(二)IDE "扎实的基础"+"宽广的视野",基本可以帮我们摆脱码畜.码奴.码农的命运! IT领袖:IT大哥:IT精英:IT ...

  4. .net基础学java系列(二)IDE

    上一篇文章.net基础学java系列(一)视野 废话: "视野"这篇文章,管理员说它比较空洞!也许初学者看不懂表格中的大部分内容!多年的neter估计也有很多不知道的! 有.net ...

  5. 三叔学FPGA系列之二:Cyclone V中的POR、配置、初始化,以及复位

    对于FPGA内部的复位,之前一直比较迷,这两天仔细研究官方数据手册,解开了心中的诸多疑惑,感觉自己又进步了呢..... 原创不易,转载请转原文,注明出处,谢谢.   一.关于POR(Power-On ...

  6. 跟我一起学Go系列:Go gRPC 安全认证方式-Token和自定义认证

    Go gRPC 系列: 跟我一起学Go系列:gRPC安全认证机制-SSL/TLS认证 跟我一起学 Go 系列:gRPC 拦截器使用 跟我一起学 Go 系列:gRPC 入门必备 接上一篇继续讲 gRPC ...

  7. [Android开发学iOS系列] Auto Layout

    [Android开发学iOS系列] Auto Layout 内容: 介绍什么是Auto Layout. 基本使用方法 在代码中写约束的方法 Auto Layout的原理 尺寸和优先级 Auto Lay ...

  8. 跟着鸟哥学Linux系列笔记3-第11章BASH学习

    跟着鸟哥学Linux系列笔记0-扫盲之概念 跟着鸟哥学Linux系列笔记0-如何解决问题 跟着鸟哥学Linux系列笔记1 跟着鸟哥学Linux系列笔记2-第10章VIM学习 认识与学习bash 1. ...

  9. 跟着鸟哥学Linux系列笔记2-第10章VIM学习

    跟着鸟哥学Linux系列笔记0-扫盲之概念 跟着鸟哥学Linux系列笔记0-如何解决问题 跟着鸟哥学Linux系列笔记1 常用的文本编辑器:Emacs, pico, nano, joe, vim VI ...

随机推荐

  1. NGK的内存为何如此的火爆?

    要说最近最受关注的公链,当属NGK了.NGK代币在迎来43倍暴涨之后似乎进入了一个平板期,这让很多投资者的热情冷却了一半,就在大家以为对NGK放弃信心时,NGK又突然爆出了一个新的炒作点:NGK内存( ...

  2. 01_MySQL从下载—>安装—>到快速上手

    一.MySQL下载 二.MySQL安装 三.MySQL几条简单命令快速上手(增删改查) 一.MySQL下载与安装 下载地址:https://dev.mysql.com/downloads/mysql/ ...

  3. Spring Security 实战干货:OAuth2登录获取Token的核心逻辑

    1. 前言 在上一篇Spring Security 实战干货:OAuth2授权回调的核心认证流程中,我们讲了当第三方同意授权后会调用redirectUri发送回执给我们的服务器.我们的服务器拿到一个中 ...

  4. 微信小程序:如何删除所有的console.log?

    使用vscode正则匹配,手动去除 1.用vscode打开微信小程序项目 2.Edit-----replace in Files 1. console.log()加了分号 console\.log\( ...

  5. lombok插件@Slf4j注解不生效问题解决办法

    最近在尝试使用日志工具Sfl4j,当时使用log时报错,找了好久才解决这个问题. 1.首先需要下载Lombok插件 File->settings->Plugins 搜索Lombok,点击安 ...

  6. WPF -- 一种圆形识别方案

    本文介绍一种圆形的识别方案. 识别流程 判断是否为封闭图形: 根据圆的方程,取输入点集中的1/6.3/6.5/6处的三个点,求得圆的方程,获取圆心及半径: 取点集中的部分点,计算点到圆心的距离与半径的 ...

  7. # PyComCAD介绍及开发方法

    项目地址:https://github.com/JohnYang1210/PycomCAD 1.综述 ​ 提到Autocad在工业界的二次开发,VB或者Lisp可能作为常用的传统的编程语言.但是,Py ...

  8. COM技术中的VARIANT and VARIANTARG

    VARIANT and VARIANTARG Use VARIANTARG to describe arguments passed within DISPPARAMS, and VARIANT to ...

  9. HDFS 03 - 你能说说 HDFS 的写入和读取过程吗?

    目录 1 - HDFS 文件的写入 1.1 写入过程 1.2 写入异常时的处理 1.3 写入的一致性 2 - HDFS 文件的读取 2.1 读取过程 2.2 读取异常时的处理 版权声明 1 - HDF ...

  10. 大括号之谜:C++的列表初始化语法解析

    有朋友在使用std::array时发现一个奇怪的问题:当元素类型是复合类型时,编译通不过. struct S { int x; int y; }; int main() { int a1[3]{1, ...