Go 语言进阶单元测试示例详解
作者:Java星辰 发布时间:2024-02-07 18:17:06
前言
本文从单元测试实践角度出发,提升对代码质量的意识。
本文内容主要包括:单元测试、Mock测试、基准测试。
测试
测试可以提高代码的质量、减少事故的发生。
测试又分为:回归测试、集成测试、单元测试。
回归测试是指对QA手动回归一些特定场景,可以理解为我们说的手动点点。
集成测试是指对系统功能维度做验证,比如对服务暴露的接口验证,一般是自动化的验证。
单元测试是指在开发阶段,开发者对单独的函数、模块做验证,写一些测试用例。
单元测试
单元测试组成部分:输入、输出、测试单元、与期望的校对,测试单元又包括函数、接口、模块、复杂的聚合函数等。
通过单元测试的输出再与期望输出进行校对,来验证代码的正确性。通过单元测试可以保证代码的质量,也可以在一定程度上提升效率,比如通过运行单元测试可以快速定位到有问题的代码。
规则
单元测试的编写有一定的规则:
所有测试文件以
_test.go
结尾测试方法名以
Test
开头,参数要用testing
func TestXxx(t *testing.T)
测试初始化逻辑放到
TestMain
中通过
go test
命令进行测试
示例
import "testing"
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutPut := "Tom"
if output != expectOutPut {
t.Errorf("Expected %s do not match actual %s", expectOutPut, output)
}
}
func HelloTom() string {
return "Jerry"
}
通过go test
命令运行得到以下结果,从测试结果里可以看出,我们期望得到的是Tom
,但实际得到的却是Jerry
。
--- FAIL: TestHelloTom (0.00s)
helloTom_test.go:9: Expected Tom do not match actual Jerry
FAIL
exit status 1
FAIL learning/mytesting 0.496s
assert
另外我们可以使用开源的assert
包,来代替我们自己的if
判断。
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutPut := "Tom"
assert.Equal(t, expectOutPut, output)
}
再次通过go test
命令运行得到以下结果,输出了更详细的堆栈信息。
覆盖率
如何评估单元测试呢?是通过代码覆盖率来评估的,评估的标准包括:
衡量代码是否经过了足够的测试
评价项目的测试水准
评估项目是否达到了高水平的测试等级
下面通过一个例子来看下代码覆盖率:
// judgepass.go
func JudgePassLine(score int16) bool {
if score >= 16 {
return true
}
return false
}
// judgepass_test.go
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
通过命令来看覆盖率go test judgepass_test.go judgepass.go --cover
输出结果为:
ok command-line-arguments 0.327s coverage: 66.7% of statements
可以看到,提示出的代码覆盖率为66.7%
,因为只走了一个if
分支。如果要想达到100%
的代码覆盖率的话,就要把所有的分支都要覆盖到。
一般的覆盖率为50%~60%
,较高的覆盖率要达到80%+
。要注意:测试分支相互独立、要全面覆盖,测试单元粒度要足够小,满足函数单一职责。
依赖
一个实际项目不可能只是一个简单的单体函数,肯定会很复杂,存在其他的依赖,比如依赖数据库、redis、文件等外部依赖。
单元测试一般有两个目标:幂等、稳定。
幂等:重复执行一个用例、调用一个接口,返回的结果是一样的。
稳定:单元测试是相互隔离的,在任何时间都能独立运行。
Mock
如果单元测试用到数据库、redis等,在单元测试里直接连接会涉及到网络传输,这是不稳定的,所以要用到Mock
机制。
开源Mock
框架:github.com/bouk/monkey
这个Mock
包可以对函数或方法进行打桩,打桩就是用一个函数A来替换一个函数B。
monkey
的实现原理主要是在运行时,通过Go
的unsafe
包能够将内存中函数的地址替换为运行时函数的地址,最终调用的是打桩函数,从而实现Mock
的功能。
Mock
常用方法:Patch
、Unpatch
。
Patch
方法有两个参数,target
为替换的函数(原函数),replacement
为要替换成的函数。
func Patch(target, replacement interface{}) *PatchGuard {
t := reflect.ValueOf(target)
r := reflect.ValueOf(replacement)
patchValue(t, r)
return &PatchGuard{t, r}
}
Unpatch
为测试结束之后,要把打的桩给卸载掉。
func Unpatch(target interface{}) bool {
return unpatchValue(reflect.ValueOf(target))
}
下面通过Mock
来模拟对文件的操作。
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
该测试用例对ProcessFirstLine
函数进行测试,这个函数调用了ReadFirstLine
函数,涉及到文件的操作,通过Mock
对文件的操作进行打桩,这样就避免了其他进程对文件操作的影响。
基准测试
Go
还提供了基准测试框架,可以测试一段程序的性能、CPU消耗,可以对代码做性能分析,测试方法与单元测试类似。
基准测试规则:
基准测试以Benchmark为前缀
需要一个*testing.B类型的参数b
基准测试必须要执行b.N次
下面通过一个模拟负载均衡的例子,来看下基准测试:
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func (pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
通过命令 go test -bench=.
运行测试,输出结果如下:
goos: darwin
goarch: amd64
pkg: learning/bench
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.70GHz
BenchmarkSelect-8 50264580 23.47 ns/op
BenchmarkSelectParallel-8 13717840 133.4 ns/op
PASS
ok learning/bench 4.559s
BenchmarkSelect-8
表示对Select
函数进行基准测试,数字8
表示 GOMAXPROCS
的值。
23.47 ns/op
表示每次调用Select
函数耗时23.47ns
。
50264580
这是50264580
次调用的平均值。
字节开源的go框架:github.com/bytedance/g…
引用 Go 语言进阶与依赖管理
来源:https://juejin.cn/post/7192759437751222331
猜你喜欢
- 1.Mysql的逻辑架构Mysql的逻辑架构如下所示,整体分为两部分,Server层和存储引擎层。与存储引擎无关的操作都是在Server层完
- 与大多数可以面向对象的编程语言不一样, PHP 是同时支持面向过程和面向对象的编程方式, PHP 开发者可以在面向过程和面向对象二者中自由选
- 测试用例我们分别在用户数据库(testpage),tempdb中创建相似对象t1,#t1,并在tempdb中创建创建非临时表,然后执行相应的
- 实现对下一个单词的预测RNN 原理自己找,这里只给出简单例子的实现代码import tensorflow as tfimport numpy
- 前言所谓“基础不狠,人站不稳”,对于任何一种编程语言来说基础往往都是重中之重,以Python为例,其
- 进度条的作用就是提示用户进度信息。可以有两种方式:1)提示完成度比如,正在安装程序的进度,一般是从0%到100%。2)提示正在进行处理比如正
- 用asp程序进行网页设计,大多因为需要访问数据库,然后再将数据显示到页面,如果数据很多的话,页面的访问速度也就变慢了,为了解决这个问题,可以
- 前言: 有时候,一个数据库有多个帐号,包括数据库管理员,开发人员,运维支撑人员等,可能有很多帐号都有比较大的权限,例如DDL操作权限(创建,
- 目录一、字典概念二、字典操作(一)创建字典1、先创建空字典,再添加元素(键值对)2、直接创建包含若干键值对的字典(二)字典操作1、读取字典元
- 由于Access数据库是一种文件型数据库,所以无法跨服务器进行访问。下面我们来介绍一下如何利用SQL Server 的链接服务器,把地理上分
- 前言:最近在学习PYQT5,感觉还挺有趣的,顺便记录一下自己的打包记录,也就当学习笔记啦,如果刚好也在学习python打包的小伙伴可以学一学
- 三种文件操作比较ioutilbufioos.File当文件较小(KB 级别)时,ioutil > bufio > os。当文件大
- 背景在本地开发vue项目的时候,当你习惯了proxyTable解决本地跨域的问题,切换到nuxt的时候,你会发现,添加了proxyTable
- 今天在看见了一堆不错的非洲的web 2.0网站的Logo,于大家一起欣赏:非洲web2.0网站的logo大部分和平时看见的web2.0网站l
- 函数作用:该函数的作用即按字面意思理解,topk:取数组的前k个元素进行排序。通常该函数返回2个值,第一个值为排序的数组,第二个值为该数组中
- 我以centos 4.4 下面的mysql 5.0.33 手工编译版本为例说明:vi /usr/local/mysql/bin/m
- 本文实例讲述了Python iter()函数用法。分享给大家供大家参考,具体如下:python中的迭代器用起来非常灵巧,不仅可以迭代序列,也
- 一.环境搭建1.下载安装包访问 Python官网下载地址:https://www.python.org/downloads/下载适合自己系统
- 原由定期更换密码是一种非常重要的安全措施,这种做法可以有效地保护你的账户和个人信息不受黑客和网络攻击者的侵害。密码泄露是一个非常普遍的问题,
- 很早就在这里看到过解决方案,与嗷嗷讨论后发现这个方案还是很可靠的。当然,唯一的缺点就是每一个属性都要去Hack,但我在很多实践中,只用‘修正