从入门到深入 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. Datahero Inc利用区块链溯源,造福各行各业

    近些年来,随着区块链技术的不断崛起以及快速发展,越多越多的人提出将区块链技术引入到溯源系统当中,溯源也成为了区块链技术的重要应用场景之一. 目前,Datahero inc已建设一整套的溯源平台系统,基 ...

  2. jetty的jndi

    jetty的jndi和tomcat的用法 tomcat的jndi是内置的,在web.xml文件里直接默认支持的,所有web项目可以直接使用 <resources> <!-- < ...

  3. 远程过程调用框架——gRPC

    gRPC是一款基于http协议的远程过程调用(RPC)框架.出自google.这个框架可以用来相对简单的完成如跨进程service这样的需求开发. 资料参考: https://blog.csdn.ne ...

  4. MySQL命名、设计及使用规范

    本文转载自MySQL命名.设计及使用规范 导语 最近在看MySQL相关的内容,整理如下规范,作为一名刚刚学习MySQL的菜鸟,整理的内容非常的基础,中间可能涉及到有错误的地方,欢迎批评指正,看到有错误 ...

  5. ELK的一点认识

    为什么需要ELK: 一般大型系统是一个分布式部署的架构,不同的服务模块部署在不同的服务器上,问题出现时,大部分情况需要根据问题暴露的关键信息,定位到具体的服务器和服务模块,构建一套集中式日志系统,可以 ...

  6. Content type 'application/json;charset=UTF-8' not supported异常的解决过程

    首先说一下当时的场景,其实就是一个很简单的添加操作,后台传递的值是json格式的,如下图 ,后台对应的实体类, @Data @EqualsAndHashCode(callSuper = false) ...

  7. Lambad表达式--Java8新特性

    1.概述 Lambda是一个匿名函数,是java8的一个新特性.可以对接口进行非常简洁的实现.但它要求接口中只能有一个抽象方法,原因是lambda只能实现一个方法.另外,需要在接口上添加注解@Func ...

  8. 痞子衡嵌入式:自识别特性(Auto Probe)可以让i.MXRT1060无需FDCB也能从NOR Flash启动

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是自识别特性(Auto Probe)可以让i.MXRT1060无需FDCB也能从NOR Flash启动. 接着上篇文章 <了解i.M ...

  9. 小白养成记——MySQL中的排名函数

    1.ROW_NUMBER() 函数 依次排序,没有并列名次.如 SELECT st.ID '学号', st.`NAME` '姓名', sc.SCORE '成绩', ROW_NUMBER() OVER( ...

  10. 五. SpringCloud服务降级和熔断

    1. Hystrix断路器概述 1.1 分布式系统面临的问题 复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败.这就造成有可能会发生服务雪崩.那么什么是服务雪崩呢 ...