3

【4-4 Golang】常用标准库—单元测试

 1 year ago
source link: https://studygolang.com/articles/35912
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【4-4 Golang】常用标准库—单元测试

tomato01 · 大约21小时之前 · 161 次点击 · 预计阅读时间 10 分钟 · 大约8小时之前 开始浏览    

  日常项目开发中,单元测试是必不可少的,Go语言本身就提供了单元测试标准库,很方便我们开展基础测试,性能测试,事例测试,模糊测试以及分析代码覆盖率,本篇文章主要介绍Go单元测试的基本操作。

单元测试概述

  "Go test" 命令可用于运行指定路径下的Go文件,这些Go文件必须以 "x_test.go" 格式命名,并且测试函数也必须以一定格式命名。"Go test" 命令编译相关Go文件,并运行对应的测试函数,最终输出测试结果,包括测试结果,包名,运行时间等,而如果执行失败,还会输出详细错误信息。

  "Go test" 命令格式可以通过help命令查看:

go help test
usage: go test [build/test flags] [packages] [build/test flags & test binary flags] [-args|-json|-c|......]

-args
        Pass the remainder of the command line (everything after -args)
-json
        Convert test output to JSON suitable for automated processing
-c
        Compile the test binary to pkg.test but do not run it

//等等

  我们举一个简单的基础测试例子,测试取绝对值函数是否正确,如下所示,基础测试函数定义必须为 func TestXxx(* testing.T),包testing定义了与单元测试有关的函数或者结构。

func TestAbs(t *testing.T) {
    got := Abs(-1)
     if got != 1 {
         t.Errorf("Abs(-1) = %d; want 1", got)
     }
}

  Go语言都支持哪几种类型的单元测试呢?主要分为基础测试,性能测试,事例测试以及模糊测试,这四种类型测试都有不同的命名格式,同样可以通过help命令查看:

go help testfunc

A test function is one named TestXxx (where Xxx does not start with a
lower case letter) and should have the signature,

    func TestXxx(t *testing.T) { ... }

A benchmark function is one named BenchmarkXxx and should have the signature,

    func BenchmarkXxx(b *testing.B) { ... }

A fuzz test is one named FuzzXxx and should have the signature,

    func FuzzXxx(f *testing.F) { ... }

Here is an example of an example:

    func ExamplePrintln() {
        Println("The output of\nthis example.")
        // Output: The output of
        // this example.
    }

  基础测试命名格式如TestXxx,这种类型测试通常用来判断结果是否符合预期,如果不符合可使用t.Errorf输出原因,也标示着此次测试结果失败;性能测试命名格式如BenchmarkXxx,将运行指定代码b.N次,输出结果包含运行次数以及平均每次耗时;事例测试命名格式如ExampleXxx,注意这种类型的测试,必须包含注释并指定输出Output,通过对比输出结果,判定测试结果是否通过;模糊测试命名格式如FuzzXxx,其可通过不通的输入值,验证你代码的正确性。

  下面将一一介绍这四种类型的单元测试。

  基础测试用于验证代码功能正确性,通过对比程序的执行结果分析程序的正确性,使用比较简单,如下是Go语言切片的一个基础测试case:

func TestAppendOverlap(t *testing.T) {
    x := []byte("1234")
    x = append(x[1:], x...) // p > q in runtime·appendslice.
    got := string(x)
    want := "2341234"
    if got != want {
        t.Errorf("overlap failed: got %q want %q", got, want)
    }
}

//ok      demo    0.503s

  如果执行结果不符合预期,输出内容是怎样的呢?如下所示:

--- FAIL: TestAppendOverlap (0.00s)
    demo_test.go:13: overlap failed: got "xxx" want "xxx"

  性能测试常用来分析对比程序性能,对一段代码,通过执行多次,计算平均耗时,以此评估程序性能;执行总时间或者执行次数可通过参数指定,支持的参数可以通过help命令查看:

go help testflag
    -bench regexp
        Run only those benchmarks matching a regular expression.
        By default, no benchmarks are run
    //性能测试执行时间;注意Nx可设置代码段执行循环次数
    -benchtime t
        Run enough iterations of each benchmark to take t, specified
        as a time.Duration (for example, -benchtime 1h30s).
        The default is 1 second (1s).
        The special syntax Nx means to run the benchmark N times
        (for example, -benchtime 100x).
    //性能测试执行次数
    -count n
        Run each test, benchmark, and fuzz seed n times (default 1).
        If -cpu is set, run n times for each GOMAXPROCS value.
        Examples are always run once. -count does not apply to
        fuzz tests matched by -fuzz.
    //设置逻辑处理器数目,默认为CPU核数
    -cpu 1,2,4
        Specify a list of GOMAXPROCS values for which the tests, benchmarks or
        fuzz tests should be executed. The default is the current value
        of GOMAXPROCS. -cpu does not apply to fuzz tests matched by -fuzz.

  还记得讲解字符串的时候提到过,Go语言字符串是只读的,不能修改的,字符串相加也是通过申请内存与数据拷贝方式实现,如果存在大量的字符串相加逻辑,每次都申请内存拷贝数据效率会非常差;而stringBuilder底层维护了一个[]byte,追加字符串只是追加到该切片,最终一次性转换该切片为字符串,避免了中间N多次的内存申请与数据拷贝,所以性能较好。如何通过性能测试验证这个结果呢?

package demo

import (
    "strings"
    "testing"
)

func BenchmarkStringPlus(b *testing.B) {
    s := ""
    for i := 0; i < b.N; i++ {
        s += "abc"
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    build := strings.Builder{}
    for i := 0; i < b.N; i++ {
        build.WriteString("abc")
    }
}

  我们编写了两个性能测试case,BenchmarkStringPlus用于测试字符串相加,BenchmarkStringBuilder用于测试stringBuilder;for循环执行次数为b.N次,可以通过参数benchtime设置。测试结果如下:

go test -benchtime 100000x  -count 3 -bench .

BenchmarkStringPlus-8            100000         15756 ns/op
BenchmarkStringPlus-8            100000         14203 ns/op
BenchmarkStringPlus-8            100000         15751 ns/op
BenchmarkStringBuilder-8         100000             4.148 ns/op
BenchmarkStringBuilder-8         100000             3.663 ns/op
BenchmarkStringBuilder-8         100000             3.372 ns/op
PASS
ok      demo    4.686s

//BenchmarkStringPlus-8 表示逻辑处理器P数目为8

  如果你的性能测试需要并行执行,可以通过RunParallel实现,其会创建多个协程执行你的代码,协程数目默认与逻辑处理器P数目保持一致,这种方式的性能测试通常需要结合-cpu参数一起使用。下面的case用于测试不同并发程度的sync.Mutex互斥锁的性能:

package demo

import (
    "sync"
    "testing"
)

func BenchmarkMutex(b *testing.B) {
    var lock sync.Mutex
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            lock.Lock()
            foo := 0
            for i := 0; i < 100; i++ {
                foo *= 2
                foo /= 2
            }
            _ = foo
            lock.Unlock()
        }
    })
}

  测试结果如下:

go test -benchtime 100000x -cpu 1,2,4,8 -bench .
BenchmarkMutex           100000            46.62 ns/op
BenchmarkMutex-2         100000            50.70 ns/op
BenchmarkMutex-4         100000            64.98 ns/op
BenchmarkMutex-8         100000           113.3 ns/op
PASS
ok      demo    0.139s

  事例测试相对也比较简单,通过在注释并指定输出Output(如果没有不执行事例测试),通过对比输出结果,判定测试结果是否通过,下面的case是一个官方自带的测试用例:

func ExampleSum256() {
    sum := sha256.Sum256([]byte("hello world\n"))
    fmt.Printf("%x", sum)
    // Output: a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
}

  普通的Output是顺序有关的,如果你的输出包括多行,顺序必须完全保持一致;如果不限制输出行顺序,可以使用Unordered output,如下面的事例,输出顺序不一样,但是统一能通过测试(如果使用output,则测试失败)。

package demo

import (
    "fmt"
)

func ExampleUnorder() {
    for _, value := range []int{1,2,3,4,0} {
        fmt.Println(value)
    }
    // Unordered output: 4
    // 2
    // 1
    // 3
    // 0
}

  模糊测试(Fuzzing)是一种通过向目标系统提供非预期的输入并监视异常结果来发现软件漏洞的方法,为什么需要模糊测试呢?因为理论上你不可能穷举所有输入作为测试用例,模糊测试的本质是依靠随机函数生成随机测试用例来进行测试验证,是不确定的。理论上只要重复测试的次数足够多,输入足够随机,更容易发现一些偶然随机错误,测试结果相对更可靠。

  与模糊测试相关的几个参数如下所示:

go help testflag
    -fuzz regexp
        Run the fuzz test matching the regular expression. When specified,
        the command line argument must match exactly one package within the
        main module, and regexp must match exactly one fuzz test within
        that package. Fuzzing will occur after tests, benchmarks, seed corpora
        of other fuzz tests, and examples have completed. See the Fuzzing
        section of the testing package documentation for details.

    //模糊测试执行时间;Nx表示执行多少次
    -fuzztime t
        Run enough iterations of the fuzz target during fuzzing to take t,
        specified as a time.Duration (for example, -fuzztime 1h30s).
        The default is to run forever.
        The special syntax Nx means to run the fuzz target N times
        (for example, -fuzztime 1000x).

    ......

  下面是Go一个模糊测试官方事例:

package main

import (
    "bytes"
    "encoding/hex"
    "testing"
)

func FuzzHex(f *testing.F) {
    //添加种子参数
    for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
        f.Add(seed)
    }
    f.Fuzz(func(t *testing.T, in []byte) {
        enc := hex.EncodeToString(in)
        out, err := hex.DecodeString(enc)
        if err != nil {
            t.Fatalf("%v: decode: %v", in, err)
        }
        if !bytes.Equal(in, out) {
            t.Fatalf("%v: not equal after round trip: %v", in, out)
        }

    })
}

  测试结果如下:

go test  -fuzztime 10000x -fuzz=FuzzHex
fuzz: elapsed: 0s, gathering baseline coverage: 0/24 completed
fuzz: elapsed: 0s, gathering baseline coverage: 24/24 completed, now fuzzing with 8 workers
fuzz: elapsed: 0s, execs: 10000 (117451/sec), new interesting: 0 (total: 24)
PASS
ok      demo    0.339s

代码覆盖率

  代码覆盖率可以用来评估我们的单元测试覆盖度,帮助我们提升代码质量,而Go语言单元测试库本身就支持代码覆盖率分析(go test -cover)。下面举一个简单的事例。

  我们的代码如下,定义了商品结构,包含商品类型以及商品名称,有一个函数可根据商品分类返回商品名称

package demo

type Product struct {
    Type int
    Name string
}

func ProductName(t int) string{
    switch t {
    case 1:
        return "手机"
    case 2:
        return "电脑"
    case 3:
        return "显示器"
    case 4:
        return "键盘"
    default:
        return "不知道"
    }
}

  测试用例如下:

package demo

import "testing"

var tests = []Product{
    {1, "手机"},
}

func TestType(t *testing.T) {
    for _, p := range tests {
        name := ProductName(p.Type)
        if name != p.Name {
            t.Errorf("ProductName(%d) = %s; want %s ", p.Type, name, p.Name)
        }
    }
}

  简单分析也可以发现,我们的测试用例不足,很多分支无法访问到。我们看下覆盖率分析结果:

go test -cover
PASS
coverage: 33.3% of statements   //只覆盖了33.3%的语句
ok      demo    3.049s

  日常项目开发中,单元测试是必不可少的,本篇文章主要介绍了如何基于Go单元测试标准库,实现基本的基础测试,性能测试,事例测试,模糊测试以及分析代码覆盖率。


有疑问加站长微信联系(非本文作者))

280

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK