GoLang 单元测试

2020-04-15 golang

程序开发中比较重要的一点是如何可以高效的进行单元测试,可以保证快速发现定位问题,在 GoLang 中自带了一个轻量级的测试框架 testing 以及 go test 命令来实现单元测试和性能测试。

简介

假设有一个简单的除法实现,对应文件 math.go

package math

import (
    "errors"
)

func Division(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divisor should not be 0")
    }

    return a / b, nil
}

与之对应的单元测试文件 math_test.go 为。

package math

import "testing"

func TestDivision(t *testing.T) {
    if rc, err := Division(6, 2); rc != 3 || err != nil {
        t.Fatal("invalid division result.")
    } else {
        t.Log("division test pass.")
    }
}

func TestDivisionFail(t *testing.T) {
    if _, err := Division(6, 0); err == nil {
        t.Error("division did not work as expected.")
    } else {
        t.Log("divisor 0 pass.", err)
    }
}

那么对应的单元测试的示例如下,需要确保遵循如下的规则:

  • 文件名需要以 _test.go 结尾,在执行 go test 时会自动匹配对应代码;
  • 测试文件引入 testing 包,且所有的测试用例必须以 Test 开头;
  • 可以通过测试函数的入参 t *testing.T 来标记错误状态信息;

在测试函数中,可以通过调用 testing.T 中定义的相关方法来标记测试信息。

  • Fail() 标记失败,但继续执行当前测试函数;
  • FailNow() 失败,立即终止当前测试函数执行;
  • Log() 输出错误或者调试信息;
  • Error() 等价于 Fail + Log
  • Fatal() 等价于 FailNow + Log
  • Skip() 跳过当前函数,通常用于未完成的测试用例。

然后可以通过 go test 执行测试,如果通过,默认只会打印通过的信息,可以通过 go test -v 查看详情。

$ go test -v
=== RUN   TestDivision
--- PASS: TestDivision (0.00s)
        math_test.go:9: division test pass.
=== RUN   TestDivisionFail
--- PASS: TestDivisionFail (0.00s)
        math_test.go:17: divisor 0 pass. divisor should not be 0
PASS
ok      _/workspace/golang/math      0.002s

在执行时,也可以通过 -run="TestTwo" 运行具体的测试用例。

覆盖率

生成覆盖率很简单,在运行 go test 时指定 -coverprofile=cover.out 参数收集覆盖率数据,然后通过 go tool cover -html=cover.out -o coverage.html 生成文本或者 html 可视化报告即可。

testify

Testify - Thou Shalt Write Tests 这是一个更加友好基于 GoLang 测试库的 Assert 方案,相比来说,提供了更加友好的提示,使用也更加广泛。

如下是官方提供的简单示例。

package yours

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestSomething(t *testing.T) {
    // assert equality
    assert.Equal(t, 123, 123, "they should be equal")

    // assert inequality
    assert.NotEqual(t, 123, 456, "they should not be equal")

    // assert for nil (good for errors)
    assert.Nil(t, object)

    // assert for not nil (good when you expect something)
    if assert.NotNil(t, object) {
        // now we know that object isn't nil, we are safe to make
        // further assertions without causing any errors
        assert.Equal(t, "Something", object.Value)
    }
}

HTTPTest

对于 Web 来说,同样提供了 httptest 的测试工具。

示例

如下是实现的一个简单的 HealthCheck 接口。

// FILE: ping.go
package main

import (
    "log"
    "net/http"
)

func PingHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"alive":true}`))
}

func main() {
    http.HandleFunc("/ping", PingHandler)
    if err := http.ListenAndServe(":12345", nil); err != nil {
        log.Fatal("Listen failed: ", err)
    }
}
// FILE: ping_test.go
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestPingHandler(t *testing.T) {
    // 用来构建模拟请求数据
    req, err := http.NewRequest("GET", "/ping", nil)
    if err != nil {
        t.Fatal(err)
    }

    // 满足ResponseWriter可以用来记录返回的响应
    rr := httptest.NewRecorder()
    PingHandler(rr, req)

    if s := rr.Code; s != http.StatusOK {
        t.Errorf("Invalid status code, %d != %d", s, http.StatusOK)
    }
    e := `{"alive":true}`
    if rr.Body.String() != e {
        t.Errorf("Invalid body, '%s' != '%s'", rr.Body.String(), e)
    }
}

然后可以通过 go test 进行测试。

Mock

官方提供了一个 gomock 库,包括一个基本的示例 sample,包含了基础的库和一个辅助代码生成工具 mockgen (一个可执行文件),可以通过如下命令安装。

go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen

然后可以通过 mockgen 命令生成 mock 对象的 .go 文件。

----- 将foo.go里的所有接口mock到foo_mock.go文件中对应的包名为foo
$ mockgen -destination foo_mock.go -source foo.go -package foo

性能测试

除了上述的单元测试,还可以通过该模块执行一些简单的压力测试,默认不会执行性能测试,可以通过 -test.bench=".*" 执行对应的压力测试用例。

例如,在 math_test.go 文件中,同时添加性能测试代码。

package math

import "testing"

func BenchmarkDivision(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Division(4, 5)
    }
}

func BenchmarkDivisionConsuming(b *testing.B) {
    b.StopTimer()
    // some init stuff.
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        Division(4, 5)
    }
}

如上性能测试中,通过 b.StopTimer() b.StartTimer() 可暂定对时间的计时,在这中间可以做些初始化的操作。

$ go test -test.bench=".*"
goos: linux
goarch: amd64
BenchmarkDivision-8             2000000000               0.39 ns/op
BenchmarkDivisionConsuming-8    2000000000               0.39 ns/op
PASS
ok      _/workspace/golang/math      1.655s

也可以通过 go test -bench=. 进行性能测试。

其它

表驱动

也就是直接定义一个临时的表结构,然后进行检查。

func TestConvert(t *testing.T) {
    cases := []struct { from, to, expected string} {
        {"50mi", "km", "80.47km" },
    }
 
    for _, c := range cases {
        str, err := Convert(c.from, c.to)
        if err != nil {
            t.Log("error should be nil", err)
            t.Fail()
        }
        if str != expected {
            t.Log("error should be"+expected+", but got", str)
            t.Fail()
        }
    }
}