0


【测试】依赖注入、表格测试与压力测试

目录

几个在Go中进行代码测试的核心技术:单元测试、压力测试与基准测试。它们共同保证了代码的准确性、可靠性与高效性。

单元测试

单元测试又叫做模块测试,它会对程序模块(软件设计的最小单位)进行正确性检验,通常,单元测试是对一个函数封装起来的最小功能进行测试。

在Go中,

testing

包为我们提供了测试的支持。要点:

  • 需要将测试函数放置到xxx\_test.go文件中
  • 测试函数以TestXxx开头,其中Xxx是测试函数的名称,以大写字母开头
  • 测试函数以 testing.T 类型的指针作为参数,可以使用这一参数在测试中打印日志、报告测试结果,或者跳过指定测试。
funcTestXxx(t *testing.T)

简单的加法例子:

// add.gopackage add 

funcAdd(a,b int)int{return a+b
}

接下来在

add_test.go

文件中,书写TestAdd测试函数,并将执行结果与预期进行对比。

// add_test.gopackage add
 
import("testing")funcTestAdd(t *testing.T){
    sum :=Add(1,2)if sum ==3{
        t.Log("the result is ok")}else{
        t.Fatal("the result is wrong")}}

要执行测试文件,可以执行go test

» go test                                                                                                      jackson@bogon
PASS
ok      github.com/dreamerjackson/xxx/add    0.013s

如果测试结果不符合预期,输出如下。

=== RUN   TestAdd
    add_test.go:13: the result is wrong
--- FAIL: TestAdd (0.00s)
 
FAIL
assert

库对testing.T进行了封装,例如函数assert.Nil 预期传入的参数为nil,而函数assert.NotNil 预期传入的参数不为nil。如果结果不符合预期,则立即报告测试失败。

不过,这样的单元测试其实并不够清晰,特别是当测试的功能逐渐变多的时候,代码还会变得冗余。 那么有没有一种测试方法可以优雅地测试多种功能呢?这就不得不提到表格驱动测试了。

表格驱动测试

表格驱动测试也是单元测试的一种,用一个例子来说明它。下面是我们写的一个字符串分割函数,它的功能类似于

strings.Split

函数。

// split.gopackage split
 
import"strings"funcSplit(s, sep string)[]string{var result []string
    i := strings.Index(s, sep)for i >-1{
        result =append(result, s[:i])
        s = s[i+len(sep):]
        i = strings.Index(s, sep)}returnappend(result, s)}
reflect.DeepEqual

是Go标准库提供的深度对比函数,它可以对比两个结构是否一致。而如果有多个要测试的用例,

reflect.DeepEqual

这段对比函数就会重复多次

package split
 
import("reflect""testing")//单元测试funcTestSplit(t *testing.T){
    got :=Split("a/b/c","/")
    want :=[]string{"a","b","c"}if!reflect.DeepEqual(want, got){
        t.Fatalf("expected: %v, got: %v", want, got)}}

在表格驱动中,使用Map或者数组来组织用例,我们只需要输入值和期望值,在下面的for循环中就能够复用对比的函数,这就让表格驱动测试在实践中非常受欢迎了。

// split_test.gopackage split
 
import("reflect""testing")funcTestSplit(t *testing.T){
    tests :=map[string]struct{
        input string
        sep   string
        want  []string}{"simple":{input:"a/b/c", sep:"/", want:[]string{"a","b","c"}},"wrong sep":{input:"a/b/c", sep:",", want:[]string{"a/b/c"}},"no sep":{input:"abc", sep:"/",   want:[]string{"abc"}},"trailing sep":{input:"a/b/c/", sep:"/", want:[]string{"a","b","c"}},}for name, tc :=range tests {
        got :=Split(tc.input, tc.sep)if!reflect.DeepEqual(tc.want, got){
            t.Fatalf("%s: expected: %v, got: %v", name, tc.want, got)}}}

子测试

前面我们看到的例子都是串行调用的,但是在一些场景下,需要通过并发调用来加速测试,这就是子测试为我们做的事情。

使用子测试可以调用

testing.T

Run

函数,子测试会新开一个协程,实现并行。除此之外,子测试还有一个特点,就是会运行所有的测试用例(即使某一个测试用例失败了)。这样在出错时,就可以将多个错误都打印出来。

如下所示,用

t.Run

子测试来测试之前的

Split

函数,并发测试所有用例。

funcTestSplit(t *testing.T){
    tests :=map[string]struct{
        input string
        sep   string
        want  []string}{"simple":{input:"a/b/c", sep:"/", want:[]string{"a","b","c"}},"wrong sep":{input:"a/b/c", sep:",", want:[]string{"a/b/c"}},"no sep":{input:"abc", sep:"/", want:[]string{"abc"}},"trailing sep":{input:"a/b/c/", sep:"/", want:[]string{"a","b","c"}},}for name, tc :=range tests {
        t.Run(name,func(t *testing.T){
            got :=Split(tc.input, tc.sep)if!reflect.DeepEqual(tc.want, got){
                t.Fatalf("expected: %#v, got: %#v", tc.want, got)}})}}

可以看到,当检测到错误时,能够清晰展示出错误用例的信息。

在这里,我们使用了

go test -run xxx

参数来指定我们要运行的程序。

-run

后面跟的是要测试的函数名,测试时会模糊匹配该函数名,符合条件的函数都将被测试。

依赖注入

当我们进行单元测试的时候,可能还会遇到一些棘手的依赖问题。

例如一个函数需要从下游的多个服务中获取信息并完成后续的操作。在测试时,如果我们需要启动这些依赖,步骤会非常繁琐,有时候甚至无法在本地实现。

因此,我们可以使用依赖注入的方式对这些依赖进行

Mock

,这种方式也能够让我们灵活地控制下游返回的数据。

我们以项目中的Flush()为例,在这个例子中,最后的 s.db.Insert 需要我们把数据插入数据库。

func(s *SQLStorage)Flush()error{iflen(s.dataDocker)==0{returnnil}deferfunc(){
        s.dataDocker =nil}()...return s.db.Insert(sqldb.TableData{
        TableName:   s.dataDocker[0].GetTableName(),
        ColumnNames:getFields(s.dataDocker[0]),
        Args:        args,
        DataCount:len(s.dataDocker),})}

但我们其实并不是真的需要一个数据库。让我们新建一个测试文件

sqlstorage_test.go

,然后实现数据库DBer接口。

// sqlstorage_test.gotype mysqldb struct{}func(m mysqldb)CreateTable(t sqldb.TableData)error{returnnil}func(m mysqldb)Insert(t sqldb.TableData)error{returnnil}

接着,我们就可以将mysqldb注入到SQLStorage结构中,单元测试如下所示。

funcTestSQLStorage_Flush(t *testing.T){type fields struct{
        dataDocker []*spider.DataCell
        options    options
    }
    tests :=[]struct{
        name    string
        fields  fields
        wantErr bool}{{name:"empty", wantErr:false},{name:"no Rule filed", fields: fields{dataDocker:[]*spider.DataCell{{Data:map[string]interface{}{"url":"<http://xxx.com>"}},}}, wantErr:true},}for_, tt :=range tests {
        t.Run(tt.name,func(t *testing.T){
            s :=&SQLStorage{
                dataDocker: tt.fields.dataDocker,
                db:         mysqldb{},
                options:    tt.fields.options,}if err := s.Flush();(err !=nil)!= tt.wantErr {
                t.Errorf("Flush() error = %v, wantErr %v", err, tt.wantErr)}
            assert.Nil(t, s.dataDocker)})}}

测试用例中测试了没有

Rule

字段时的情形,但是程序却直接

panic

了。这就是单元测试的意义所在,它可以为我们找到一些特殊的输入,确认它们是否仍然符合预期。

经过测试我们发现,由于我们将接口强制转换为了string,当接口类型不匹配时就会直接panic。

ruleName := datacell.Data["Rule"].(string)
taskName := datacell.Data["Task"].(string)

要避免这种情况,我们可以对异常情况进行判断:

if ruleName, ok = datacell.Data["Rule"].(string);!ok {return errors.New("no rule field")}if taskName, ok = datacell.Data["Task"].(string);!ok {return errors.New("no task field")}

压力测试

有时候,我们还希望对程序进行压力测试,它可以测试随机场景、排除偶然因素、测试函数稳定性等等。

实现压力测试的方法和工具有很多,例如ab、wrk。合理的压力测试通常需要结合实际项目来设计。
我们也可以通过书写Shell脚本来进行压力测试,如下脚本中, 我们可以用

go test -c

为测试函数生成二进制文件,并循环调用测试函数。

# pressure.sh
go test-c# -c会生成可执行文件PKG=$(basename$(pwd))# 获取当前路径的最后一个名字,即为文件夹的名字echo$PKGwhiletrue;doexportGOMAXPROCS=$[1 + $[RANDOM % 128]]# 随机的GOMAXPROCS
        ./$PKG.test $@2>&1# $@代表可以加入参数   2>&1代表错误输出到控制台done

以之前的加法函数为例,执行下面的命令即可对测试函数进行压力测试。其中,

-test.v

为运行参数,用于输出详细信息。

> /pressure.sh -test.v
 
PASS
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
    add_test.go:17: the result is ok
PASS
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
    add_test.go:17: the result is ok
 

基准测试

Go测试包中内置了Benchmarks基准测试,它可以对比改进后和改进前的函数,查看性能提升效果,也可以供我们探索一些Go的特性。

我们可以用基准测试来对比之前的接口调用与直接函数调用。

package escape
 
import"testing"type Sumifier interface{Add(a, b int32)int32}type Sumer struct{ id int32}func(math Sumer)Add(a, b int32)int32{return a + b }type SumerPointer struct{ id int32}func(math *SumerPointer)Add(a, b int32)int32{return a + b }funcBenchmarkDirect(b *testing.B){
    adder := Sumer{id:6754}
    b.ResetTimer()for i :=0; i < b.N; i++{
        adder.Add(10,12)}}funcBenchmarkInterface(b *testing.B){
    adder := Sumer{id:6754}
    b.ResetTimer()for i :=0; i < b.N; i++{Sumifier(adder).Add(10,12)}}funcBenchmarkInterfacePointer(b *testing.B){
    adder :=&SumerPointer{id:6754}
    b.ResetTimer()for i :=0; i < b.N; i++{Sumifier(adder).Add(10,12)}}

go test 可以加入

-gcflags

指定编译器的行为。例如这里的

-gcflags “-N -l”

表示禁止编译器的优化与内联,

-bench=.

表示执行基准测试,这样我们就可以对比前后几个函数的性能差异了。

» go test -gcflags "-N -l"   -bench=.
BenchmarkDirect-12                      535487740                1.95 ns/op
BenchmarkInterface-12                   76026812                 14.6 ns/op
BenchmarkInterfacePointer-12            517756519                2.37 ns/op
 

BenchMark测试时还可以指定一些其他运行参数,例如-benchmem可以打印每次函数的内存分配情况,-cpuprofile、-memprofile还能收集程序的 CPU 和内存的 profile 文件。

go test ./fibonacci \\
  -bench BenchmarkSuite \\
  -benchmem \\
  -cpuprofile=cpu.out \\
  -memprofile=mem.out
 

这些生成的样本文件我们可以使用pprof工具进行可视化分析。关于pprof工具,我们在之后还会做详细介绍。

总结

介绍了Go中的多种测试技术,包括单元测试、表格驱动测试、子测试、基准测试、压力测试、依赖注入等。灵活地使用这些测试技术可以提前发现系统存在的性能问题。


Note: 极客时间课程笔记


本文转载自: https://blog.csdn.net/qq_42899028/article/details/131651650
版权归原作者 jayvee_ 所有, 如有侵权,请联系我们删除。

“【测试】依赖注入、表格测试与压力测试”的评论:

还没有评论