还在为团队 Bug 频现而烦恼吗?提测总被测试打回困扰吗?降低千行代码bug率,势在必行!基于 go test 做自动化 Case 覆盖

Posted 魏小言

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了还在为团队 Bug 频现而烦恼吗?提测总被测试打回困扰吗?降低千行代码bug率,势在必行!基于 go test 做自动化 Case 覆盖相关的知识,希望对你有一定的参考价值。


基于 go test 做自动化 Case 覆盖

你还在为团队 Bug 频现而烦恼吗?提测总被测试打回困扰吗?降低千行代码 bug 率,势在必行!

虽然说用 “ 千行代码 bug 率 ” 来衡量个人、团队、服务是否优秀?有失偏颇!但为了预防错误频生、确保生产安全,降低千行代码 bug 率,势在必行!

自动化 Case 覆盖

通过 “ 测试自动化 Case 覆盖 ” ,可以提高测试覆盖率,降低千行代码 bug 率,有效提升服务质量!

如果你现在接手的项目还没有考量 Case 覆盖率,那么感兴趣的话可以拿去!

测试自动化 Case 覆盖 ,很简单,这里主要分享一下具体策略及 Go 测试基本知识。

Testing

利用 Go 自带的 testing 功能包,对服务入口函数设计单元测试。

说到 Case 覆盖率,有些同学可能不陌生。在单元测试中,通过 cover 命令字段,可以看到 测试文件具体的覆盖率。

对,这里用的也是这个东东。

这里是把这个 “单元测试 ” 中的 “ 单元 ” 放大为整个服务。我们一般做单元测试,是针对某个函数,这些函数往往是功能对象的行为方式。其实在单元测试中,单元可大可小,由具体诉求确定,这里我们把 “ 单元 ” 设定为 服务整体,即 服务入口 “ main “ 函数。

main_test.go

既然测试目标函数是 main,那么我们编写 main_test.go 文件,编码如下:

package main

import (
   "flag"
   "fmt"
   "os"
   "testing"
)

var configPath string

func TestMain(m *testing.M) {
   flag.StringVar(&configPath, "config", "", "config path")
   flag.Parse()
   if len(configPath) == 0 {
      fmt.Println("config path is invalid.", configPath)
      return
   }
   os.Exit(m.Run())
}

func Test_Main(t *testing.T) {
   cliOpt := CliOption{CfgFname: configPath}
   doCommand(&cliOpt)
}

可以看到,这里我们用到了 testing 中的 testing.M 和 testing.T 两个方式。

testing.M 通常用于配合 TestMain() 使用。

  • TestMain() 命名规则来看,是针对 Main.go 文件中的 main 函数做测试。
  • 其实它在 testing 包中还有额外的用处:
    • TestMain(m *testing.M) 作为 TestXXX(t *testing.T) 的入口,为 TestXXX() 做对象初始化及善后工作。
      • testing.M\\testing.T 是不同的 struct ,M 相对 T 缺少部分方法及实例,无法完成 testing case 数据统计及日志输出等工作。
      • testing.M 通过调用 m.Run() ,在执行真正的 testing.T 操作,并以其结果 Exit() 做退出。

注意:m.Run() 内部隐含 flag.Parse(),上文中是显示调用,不会与 Run() 中冲突。

testing.M
// M is a type passed to a TestMain function to run the actual tests.
type M struct {
 deps       testDeps
 tests      []InternalTest
 benchmarks []InternalBenchmark
 examples   []InternalExample


 timer     *time.Timer
 afterOnce sync.Once


 numRun int


 // value to pass to os.Exit, the outer test func main
 // harness calls os.Exit with this code. See #34129.
 exitCode int
}
testing.T
// T is a type passed to Test functions to manage test state and support formatted test logs.
//
// A test ends when its Test function returns or calls any of the methods
// FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods, as well as
// the Parallel method, must be called only from the goroutine running the
// Test function.
//
// The other reporting methods, such as the variations of Log and Error,
// may be called simultaneously from multiple goroutines.
type T struct {
 common
 isParallel bool
 context    *testContext // For running tests and subtests.
}

集成 build.sh

有了 testing 入口 main_test.go ,那么如何做成自动化呢?

很简单,我们把 testing 集成至 build.sh ,每次执行 build.sh 即可,编码如下:

function func_coverage(){
  set -e
  mkdir -p bin tmp
  go test -c -o bin/abagent_coverage -covermode=count -coverpkg ./...;
  # 使用
    # ./bin/abagent_coverage --config configs/abagent.test.toml -test.coverprofile=tmp/abagent_coverage.out
    # 对 test.toml 端口发起请求
    # kill 服务
    # 输出报告
    # go tool cover -html tmp/abagent_coverage.out -o tmp/abagent_coverage.html
    # 查看报告
    # python -m SimpleHTTPServer 1238
    # 打开 http://127.0.0.1:1238/tmp/abagent_coverage.html
}

这个函数将 testing 构建成可执行文件,通过指定路径、名称、testing 覆盖范围,进行服务启动执行。

具体执行步骤如下:

# 使用
    # ./bin/abagent_coverage --config configs/abagent.test.toml -test.coverprofile=tmp/abagent_coverage.out
    # 对 test.toml 端口发起请求
    # kill 服务
    # 输出报告
    # go tool cover -html tmp/abagent_coverage.out -o tmp/abagent_coverage.html
    # 查看报告
    # python -m SimpleHTTPServer 1238
    # 打开 http://127.0.0.1:1238/tmp/abagent_coverage.html

通过完善服务请求,可逐步增加 Case 覆盖率,一般除去异常分支、初始化部分代码,覆盖率在 ~90%+ 较为合理。

go test 指令

这里对上面用到的 go test 指令做一个简单的梳理,如下:

标记名称标记描述
-c生成用于运行测试的可执行文件,但不执行它。这个可执行文件会被命名为 “pkg.test” ,其中的 “pkg” 即为被测试代码包的导入路径的最后一个元素的名称。
-i安装/重新安装运行测试所需的依赖包,但不编译和运行测试代码。
-o指定用于运行测试的可执行文件的名称。追加该标记不会影响测试代码的运行,除非同时追加了标记 -c 或 -i 。

除此之外,go test命令还有很多功效各异的标记。但是由于这些标记的复杂性,我们需要结合测试源码文件进行详细的讲解。所以我们在这里略过不讲。

flag

testing 中用 flag 做命令行及映射关系的解析,这里将 flag 相关指令做整理,如下:

Usage of ./bin/abagent_coverage:
  -application string
        assist for application pool config.
  -c string
        motan run conf
  -eport int
        agent export service port when as a reverse proxy server
  -hport int
        http forward proxy server port
  -idc string
        the idc info for agent or client.
  -localIP string
        local ip for motan register
  -log_async
        If false, write log sync, default is true (default true)
  -log_dir string
        If non-empty, write log files in this directory (default ".")
  -log_level string
        Init log level, default is info. (default "info")
  -log_structured
        If true, write accessLog structured, default is false
  -mport int
        agent manage port
  -pidfile string
        agent manage port
  -pool string
        application pool config. like 'application-idc-level'
  -port int
        agent listen port
  -recover
        recover from accidental exit
  -rotate_per_hour
         (default true)
  -test.bench regexp
        run only benchmarks matching regexp
  -test.benchmem
        print memory allocations for benchmarks
  -test.benchtime d
        run each benchmark for duration d (default 1s)
  -test.blockprofile file
        write a goroutine blocking profile to file
  -test.blockprofilerate rate
        set blocking profile rate (see runtime.SetBlockProfileRate) (default 1)
  -test.count n
        run tests and benchmarks n times (default 1)
  -test.coverprofile file
        write a coverage profile to file
  -test.cpu list
        comma-separated list of cpu counts to run each test with
  -test.cpuprofile file
        write a cpu profile to file
  -test.failfast
        do not start new tests after the first test failure
  -test.list regexp
        list tests, examples, and benchmarks matching regexp then exit
  -test.memprofile file
        write an allocation profile to file
  -test.memprofilerate rate
        set memory allocation profiling rate (see runtime.MemProfileRate)
  -test.mutexprofile string
        write a mutex contention profile to the named file after execution
  -test.mutexprofilefraction int
        if >= 0, calls runtime.SetMutexProfileFraction() (default 1)
  -test.outputdir dir
        write profiles to dir
  -test.paniconexit0
        panic on call to os.Exit(0)
  -test.parallel n
        run at most n tests in parallel (default 8)
  -test.run regexp
        run only tests and examples matching regexp
  -test.short
        run smaller test suite to save time
  -test.testlogfile file
        write test action log to file (for use only by cmd/go)
  -test.timeout d
        panic test binary after duration d (default 0, timeout disabled)
  -test.trace file
        write an execution trace to file
  -test.v
        verbose: print additional output

Q&A

1、看起来没什么,就是对函数入口做 testing,然后集成到 build.sh?

对,确实是这样,用到的都是 go test 基础指令。

2、执行完脚本,仍需要借助 go tool 手动执行,不见得是自动化的呢?

对,其实质是利用 go test 生成服务模块各个文件的 case 覆盖率。
Go tool 是基于覆盖率分析,以 UI 或文本 等形式表现。

3、做这个有很大必要吗?感觉 QA 做更专业

确实,QA 拥有专业的技能去保证生产缺陷可控。
做这个可最大保证提测前的代码质量,且此可最大程度的反哺给 QA 同学 case 相关数据,提升服务持续交付的效能!

附录

学会宽容,选择遗忘,将心比心!

以上是关于还在为团队 Bug 频现而烦恼吗?提测总被测试打回困扰吗?降低千行代码bug率,势在必行!基于 go test 做自动化 Case 覆盖的主要内容,如果未能解决你的问题,请参考以下文章

DynamicTp v1.0.7版本发布。还在为Dubbo线程池耗尽烦恼吗?还在为Mq消费积压烦恼吗?

曹虞:你还在为套单所烦恼吗?

一文搞定Java集合类,你还在为Java集合类而烦恼吗?

AirSim动态 | 还在为AirSim中配置传感器而烦恼吗?

dart系列之:还在为编码解码而烦恼吗?用dart试试

还在为Android表情开发烦恼吗,快来试试Android Emoji吧