go测试&项目需求分析与拆分 | 青训营笔记

103 阅读3分钟

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机制。

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
     }
 }