go测试&项目需求分析与拆分 | 青训营笔记
这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
测试
测试在流程中位于开发之后,是避免事故的最后一道屏障。
测试分回归测试,集成测试和单元测试。单元测试的覆盖率一定程度上决定了代码的质量。
单元测试
单元测试主要包括输入,测试单元,输出以及校对。单元的概念比较广,包括接口、函数、模块等;用最后的校对来保证代码的功能与我们的预期相符。
基本规范
- 所有测试文件以
_test.go结尾 - 函数名为
func TestXxx(*testing.T) - 初始化逻辑放到
TestMain中
示例
源代码的函数长这样,很明显写错了:
func HelloTom() string {
return "Jerry"
}
现在在同级目录下新建xxxx_test.go文件,写入以下代码并运行该文件。
package main
import "testing"
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %v do not match actual %v", expectOutput, output)
}
}
控制台就报出了如下错误
=== RUN TestHelloTom
main_test.go:9: Expected Tom do not match actual Jerry
--- FAIL: TestHelloTom (0.00s)
FAIL
同样的道理,如果代码本来就正确或者代码修改成功,控制台就会输出PASS
=== RUN TestHelloTom
--- PASS: TestHelloTom (0.00s)
PASS
也可以使用assert判断,上面是直接用!=,assert这个包里也提供了解决方案:assert.Equal(t, output, expectOutput)。这个包还有很多其他判断是否通过测试的方法。
覆盖率
- 如何测量代码是否经过了足够的测试?
- 如何评价项目的测试水准?
- 如何评估项目是否达到了高水准测试等级?
想要显示覆盖率,在测试命令行后加上参数--cover即可。Goland也可以直接选择使用覆盖率运行代码。我们的目标,就是创建尽可能多的测试分支,提高覆盖率。
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立,全面覆盖
- 测试单元粒度足够小,函数单一职责
Mock
实际项目往往会依赖许多东西,而单元测试需要保证稳定性和幂等性。稳定是指相互隔离,能在任何情况下运行测试,幂等性是指每一次测试运行都应该产生一样的结果。实现这一目的需要用到mock机制。
示例
以下函数对本地文件有强依赖,若本地文件被篡改则无法运行
func ProcessFirstLine() string {
Line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
通过Mock来打桩函数,默认返回line110。这里通过defer写在mock,这样整个测试函数就摆脱了本地文件的束缚和依赖。
func TestProcessFirstLine(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
}
defer monkey.UnPatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
基准测试
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
基本规范
和单元测试大同小异。基准测试以Benchmark开头,入参是testing.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)]
}
ResetTimer用于重置计时器,因为做了初始化等操作,这些操作不应该作为基准测试的范围。runparallel是多协程并发测试。
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()
}
})
}
运行结果:
BenchmarkSelect-12 84869830 14.11 ns/op
BenchmarkSelectParallel-12 15821240 74.80 ns/op
发现代码在并发情况下存在劣化,不符合常识,主要因为rand的原因。将rand换为fastrand性能就大幅提升。
为了解决这个随机性能问题,由公司开源了一个高性能随机数方法fastrand。
重新运行后的结果:
BenchmarkSelect-12 273700900 4.412 ns/op
BenchmarkSelectParallel-12 1000000000 0.6282 ns/op
项目实践
- 数据层:Model-Repository,外部数据的增删改查
- 逻辑层:Entity-Service,处理核心业务逻辑输出
- 视图层:View-Controller,处理和外部的交互逻辑
- Gin高性能go web框架, 链接
- Go Mod,
go mod init; go get gopkg.in/gin-gonic/gin.v1@v1.3.0
当所有层都实现完后,如何启动项目?
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
示例:
func main() {
if errr := Init("./data/"); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := controller.QueryPageInfo(topicId)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}