简谈Go的测试

111 阅读5分钟

什么是测试

测试,顾名思义,将写好的业务逻辑进行测试,以保证在投入生产环境时出现业务上的错误。

当然为了水水字数(乐),引用一下维基百科的解释:

软件测试(英语:software testing),描述一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程。依照可计算理论(计算机科学的一个支派)一个简单的数学证明推断出下列结果:不可能完全解决所谓“死机”,指任意计算机程序是否会进入死循环,或者罢工并产生输出问题。换句话说,软件测试是一种实际输出与预期输出间的审核或者比较过程。

举个例子:

一个测试工程师走进一家酒吧,要了一杯啤酒

一个测试工程师走进一家酒吧,要了一杯咖啡

一个测试工程师走进一家酒吧,要了0.7杯啤酒

一个测试工程师走进一家酒吧,要了-1杯啤酒

一个测试工程师走进一家酒吧,要了2^32杯啤酒

一个测试工程师走进一家酒吧,要了一杯洗脚水

一个测试工程师走进一家酒吧,要了一杯蜥蜴

一个测试工程师走进一家酒吧,要了一份asdfQwer@24dg!&*(@

一个测试工程师走进一家酒吧,什么也没要

一个测试工程师走进一家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来

一个测试工程师走进一家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿

一个测试工程师走进一

一个测试工程师走进一家酒吧,要了一杯烫烫烫的锟斤拷

一个测试工程师走进一家酒吧,要了NaN杯Null

1T测试工程师冲进一家酒吧,要了500T啤酒咖啡洗脚水野猫狼牙棒奶茶

1T测试工程师把酒吧拆了

一个测试工程师化装成老板走进一家酒吧,要了500杯啤酒并且不付钱

一万个测试工程师在酒吧门外呼啸而过

一个测试工程师走进一家酒吧,要了一杯啤酒';DROP TABLE 酒吧

测试工程师们满意地离开了酒吧。然后一名顾客点了一份炒饭,酒吧炸了

上面的笑话已经是经典例子了,在上面的场景中不光涉及了单元测试,也涉及到了压力测试。

  • 单元测试:是针对程序的模块进行正确性的测试工作,例如在酒吧里面最小的功能便是要一杯酒;
  • 压力测试:是确定软件稳定性的一种方法,通常会让软件在超过正常运作条件外进行运作。例如上文有1T的测试工程师要了500T的啤酒。

测试的方式是五花八门的,但是目的都只有一个:发现错误。 对于测试来说,它要模拟一切可能的情况来发现软件的毛病,就像挑刺一样。而在测试过程当中发现的问题越多,在生产环境中软件的问题就越少。

Go的测试

testing的简单例子

在Go中,标准库内就有着这样一个包testing为我们提供了测试需要的工具。同时Go也推荐测试文件和源代码文件放在一块,测试文件一定是以_test.go结尾。例如在foo包,我们想测试foos中的F函数,那我们需要在foos.go的同级目录下新建一个foos_test.go

foo

├─foos.go

└foos_test.go

foos.go

package foo
​
import "fmt"func F() string {
    fmt.Println("I am a function called F.")
    return "F"
}

那么我们便可以编写如下的测试代码:

package foo
​
import "testing"func TestF(t *testing.T) {
    if name := F(); name != "F" {
        t.Errorf("expected name is F but got %s", name)
    }
}

在编写测试代码的时候我们要注意以下几点:

  • 测试用例名称一般命名为 Test 加上待测试的方法名。
  • 测试用的参数有且只有一个,在这里是 t *testing.T
  • 基准测试(benchmark)的参数是 *testing.B,对主函数TestMain的参数是 *testing.M 类型。

在工作目录下打开终端,输入go test,这会让此package下的所有测试同时运行。观察结果:

$ go test
​
I am a function called F.
PASS
ok      foo     0.288s

如果想查看每个用例的测试结果,加上-v参数:

$ go test -v
​
=== RUN   TestF
I am a function called F.
--- PASS: TestF (0.00s)
PASS
ok      foo     0.281s

如果想要查看测试的覆盖率,可以加上-cover参数:

$ go test -cover
​
I am a function called F.
PASS
        foo     coverage: 100.0% of statements
ok      foo     0.299s

覆盖率简单来理解,就是单元测试中代码执行量与代码总量之间的比率。因为我们的F()函数所有行的代码都被执行过了,所以此处的覆盖率是100%(当然,覆盖率不能简单代表测试的完整性,它只是其中的一个衡量标准!)

如果只想运行其中的一个用例,可以使用-run参数来指定,该参数支持通配符 *,和部分正则表达式,例如 ^$

$ go test -run TestF.
​
I am a function called F.
PASS
ok      foo     0.272s

testing的子测试

在一个测试函数中,我们可以创建不同的测试样例来检测逻辑的合理性而不是创建多个测试函数来进行测试。

我们来重新写个函数:

func QuickPow(a, b int64, mod int64) int64 {
    var result int64 = 1
    for b > 0 {
        if b&1 == int64(1) {
            result = result * a % mod
        }
        a = a * a % mod
        b >>= 1
    }
    return result
}

这个函数是在以O(log n)O(log\ n)的时间复杂度下快速计算:ab mod ma^b\ mod \ m的一个函数,笔者在此叫它快速幂,我们针对此函数来写一下单元测试:

func TestQuickPow(t *testing.T) {
    t.Run("2^2 % 3", func(t *testing.T) {
        if res := QuickPow(2, 2, 3); res != 1 {
            t.Errorf("expected 1 but actual is %d", res)
        }
    })
​
    t.Run("100003^256 % 987654", func(t *testing.T) {
        if res := QuickPow(100003, 256, 987654); res != 301705 {
            t.Errorf("expected 301705 but actual is %d", res)
        }
    })
}

在此处我们编写了两个测试用例,一个用于计算 22 mod 12^2\ mod \ 1,一个用于计算100003256 mod 987654100003^256 \ mod \ 987654,运行测试:

$ go test -run TestQuickPow -v -cover

=== RUN   TestQuickPow
=== RUN   TestQuickPow/2^2_%_3
=== RUN   TestQuickPow/100003^256_%_987654
--- PASS: TestQuickPow (0.00s)
    --- PASS: TestQuickPow/2^2_%_3 (0.00s)
    --- PASS: TestQuickPow/100003^256_%_987654 (0.00s)
PASS
        foo     coverage: 77.8% of statements
ok      foo     0.299s

Benchmark 基准测试

基准测试用例的定义如下:

func BenchmarkName(b *testing.B){
    // ...
}
  • 函数名必须以 Benchmark 开头,后面一般跟待测试的函数名
  • 参数为 b *testing.B
  • 执行基准测试时,需要添加 -bench 参数。

我们来做一下快速幂的基准测试:

func BenchmarkQuickPow(b *testing.B) {
	//如果你需要做点预先配置的话可以使用b.ResetTimer()来重置计时器
    //这里生成一些大数用
	var mods int64 = 998244353
	var a []int64
	var s []int64
	for i := 0; i < b.N; i++ {
		a = append(a, rand.Int63())
		s = append(s, rand.Int63())
	}
	//从这一行以后才开始计算时间
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		QuickPow(a[i], s[i], mods)
	}
}

运行:

$ go test -benchmem -bench BenchmarkQuickPow

I am a function called F.
goos: windows
goarch: amd64
pkg: foo
cpu: 12th Gen Intel(R) Core(TM) i7-12700H
BenchmarkQuickPow-20            51513421                24.14 ns/op            0 B/op          0 allocs/op
PASS
ok      foo     5.750s

当然我们只看这一行:

BenchmarkQuickPow-20            51513421                24.14 ns/op            0 B/op          0 allocs/op

这一行从左到右都是在打印这个结构体:

type BenchmarkResult struct {
    N         int           // 迭代次数
    T         time.Duration // 基准测试花费的时间
    Bytes     int64         // 一次迭代处理的字节数
    MemAllocs uint64        // 总的分配内存的次数
    MemBytes  uint64        // 总的分配内存的字节数
}

参考