Golang單元測試

語言: CN / TW / HK
      • 1.1 什麼是單元&單元測試
      • 1.2 為什麼進行單元測試
      • 1.3 單元測試用例編寫的原則
    • 2、golang 常用的單測框架

1、單元測試概述

1.1 什麼是單元&單元測試

  • 單元是應用的最小可測試部件,如函式和物件的方法
  • 單元測試是軟體開發中對最小單位進行正確性檢驗的測試工作

1.2 為什麼進行單元測試

  • 保證變更/重構的正確性,特別是在一些頻繁變動和多人合作開發的專案中
  • 簡化除錯過程: 可以輕鬆的讓我們知道哪一部分程式碼出了問題
  • 單測最好的文件:在單測中直接給出具體介面的使用方法,是最好的例項程式碼

1.3 單元測試用例編寫的原則

  • 單一原則:一個測試用例只負責一個場景
  • 原子性:結果只有兩種情況: PassFail
  • 優先要核心元件和邏輯的測試用例
  • 高頻使用庫, util ,重點覆蓋

1.4 單測用例規定

xx_test.go
TestXXX
t *testing.T

2、golang 常用的單測框架

2.1 testing

http://golang.google.cn/pkg/testing/

2.1.1 單元測試

Go 提供了 test 工具用於程式碼的單元測試, test 工具會查詢包下以 _test.go 結尾的檔案,呼叫測試檔案中以 TestBenchmark 開頭的函式並給出執行結果

測試函式需要匯入 testing 包,並定義以 Test 開頭的函式,引數為 testing.T 指標型別,在測試函式中呼叫函式進行返回值測試,當測試失敗可通過 testing.T 結構體的 Error 函式丟擲錯誤

單元測試是對某個功能的測試

命令列執行

go test 包名  # 測試整個包
go test -v .
go test 包名/檔名  # 測試某個檔案

簡單使用

準備待測程式碼 compute.go

package pkg03

func Add(a, b int) int {
	return a + b
}

func Mul(a, b int) int {
	return a * b
}

func Div(a, b int) int {
	return a / b
}

準備測試用例 compute_test.go

package pkg03

import "testing"

func TestAdd(t *testing.T) {
	a := 10
	b := 20
	want := 30
	actual := Add(a, b)
	if want != actual {
		t.Errorf("Add函式引數:%d %d, 期望: %d, 實際: %d", a, b, want, actual)
	}
}

func TestMul(t *testing.T) {
	a := 10
	b := 20
	want := 300
	actual := Mul(a, b)
	if want != actual {
		t.Errorf("Mul函式引數:%d %d, 期望: %d, 實際: %d", a, b, want, actual)
	}
}

func TestDiv(t *testing.T) {
	a := 10
	b := 20
	want := 2
	actual := Div(a, b)
	if want != actual {
		t.Errorf("Div函式引數:%d %d, 期望: %d, 實際: %d", a, b, want, actual)
	}
}

執行測試

➜  pwd                    
golang-learning/chapter06/pkg03
➜  go test -v .
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestMul
    compute_test.go:21: Mul函式引數:10 20, 期望: 300, 實際: 200
--- FAIL: TestMul (0.00s)
=== RUN   TestDiv
    compute_test.go:31: Div函式引數:10 20, 期望: 2, 實際: 0
--- FAIL: TestDiv (0.00s)
FAIL
FAIL    pkg03   0.198s
FAIL

只執行某個函式

go test -run=TestAdd -v .
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      pkg03   0.706s

正則過濾函式名

go test -run=TestM.* -v .

2.1.2 測試覆蓋率

用於統計目標包有百分之多少的程式碼參與了單測

使用 go test 工具進行單元測試並將測試覆蓋率覆蓋分析結果輸出到 cover.out 檔案

例如上面的例子

go test -v -cover
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestMul
    compute_test.go:21: Mul函式引數:10 20, 期望: 300, 實際: 200
--- FAIL: TestMul (0.00s)
=== RUN   TestDiv
    compute_test.go:31: Div函式引數:10 20, 期望: 2, 實際: 0
--- FAIL: TestDiv (0.00s)
FAIL
coverage: 100.0% of statements
exit status 1
FAIL    pkg03   0.185s

生成測試覆蓋率檔案

go test -v -coverprofile=cover.out
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestAddFlag
--- PASS: TestAddFlag (0.00s)
PASS
coverage: 75.0% of statements
ok      testcalc/calc   0.960s

分析測試結果,開啟測試覆蓋率結果檔案,檢視測試覆蓋率

go tool cover -html cover.out

2.1.3 子測試t.run

func TestMul2(t *testing.T) {
	t.Run("正數", func(t *testing.T) {
		if Mul(4, 5) != 20 {
			t.Fatal("muli.zhengshu.error")
		}
	})
	t.Run("負數", func(t *testing.T) {
		if Mul(2, -3) != -6 {
			t.Fatal("muli.fushu.error")
		}
	})
}

執行測試

➜  go test -v .
=== RUN   TestMul2
=== RUN   TestMul2/正數
=== RUN   TestMul2/負數
--- PASS: TestMul2 (0.00s)
    --- PASS: TestMul2/正數 (0.00s)
    --- PASS: TestMul2/負數 (0.00s)

指定 func/sub 執行子測試

➜  go test -run=TestMul2/正數 -v
=== RUN   TestMul2
=== RUN   TestMul2/正數
--- PASS: TestMul2 (0.00s)
    --- PASS: TestMul2/正數 (0.00s)
PASS
ok      pkg03   0.675s

子測試的作用: table-driven tests

2.2 goconvey

goconvey 是一個第三方測試框架,其最大好處就是對常規的 if else 進行了高度封裝

2.2.1 基本使用

準備待測程式碼 student.go

package pkg04

import "fmt"

type Student struct {
	Name      string
	ChiScore  int
	EngScore  int
	MathScore int
}

func NewStudent(name string) (*Student, error) {
	if name == "" {
		return nil, fmt.Errorf("name為空")
	}
	return &Student{
		Name: name,
	}, nil
}

func (s *Student) GetAvgScore() (int, error) {
	score := s.ChiScore + s.EngScore + s.MathScore
	if score == 0 {
		return 0, fmt.Errorf("全都是0分")
	}
	return score / 3, nil
}

參考官方示例,準備測試用例 student_test.go
直觀來講,使用 goconvey 的好處是不用再寫多個 if 判斷

package pkg04

import (
	. "github.com/smartystreets/goconvey/convey"
	"testing"
)

func TestNewStudent(t *testing.T) {
	Convey("start test new", t, func() {
		stu, err := NewStudent("")
		Convey("空的name初始化錯誤", func() {
			So(err, ShouldBeError)
		})
		Convey("stu物件為nil", func() {
			So(stu, ShouldBeNil)
		})
	})
}

func TestScore(t *testing.T) {
	stu, _ := NewStudent("hh")
	Convey("不設定分數可能出錯", t, func() {
		sc, err := stu.GetAvgScore()
		Convey("獲取分數出錯了", func() {
			So(err, ShouldBeError)
		})
		Convey("分數為0", func() {
			So(sc, ShouldEqual, 0)
		})
	})
	Convey("正常情況", t, func() {
		stu.ChiScore = 60
		stu.EngScore = 70
		stu.MathScore = 80
		score, err := stu.GetAvgScore()
		Convey("獲取分數出錯了", func() {
			So(err, ShouldBeNil)
		})
		Convey("平均分大於60", func() {
			So(score, ShouldBeGreaterThan, 60)
		})
	})
}

執行 go test -v .

➜  go test -v .
=== RUN   TestNewStudent

  start test new 
    空的name初始化錯誤 ✔
    stu物件為nil ✔


2 total assertions

--- PASS: TestNewStudent (0.00s)
=== RUN   TestScore

  不設定分數可能出錯 
    獲取分數出錯了 ✔
    分數為0 ✔


4 total assertions


  正常情況 
    獲取分數出錯了 ✔
    平均分大於60 ✔


6 total assertions

--- PASS: TestScore (0.00s)
PASS
ok      pkg04   0.126s

2.2.2 圖形化使用

  • 確保本地有 goconvey 的二進位制
go get github.com/smartystreets/goconvey
# 會將對應的二進位制檔案放到 $GOPATH/bin 下面
  • 編輯環境變數把 GOPATH/bin 加入 PATH 裡面 或者寫全路徑
  • 到測試的目錄下,執行 goconvey ,啟動 http 8000 ,自動執行測試用例
  • 瀏覽器訪問 http://127.0.0.1:8000

最終效果如下

2.3 testify

2.3.1 簡單使用

業務程式碼 cal.go

package pkg05

func Add(x int ) (result int) {
	result = x + 2
	return result
}

測試用例 cal_test.go

package pkg05

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestAdd(t *testing.T) {
	// assert equality
	assert.Equal(t, Add(5), 7, "they should be equal")
}

執行測試

➜  go test -v .
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      pkg05   1.216s

2.3.2 表驅動測試

package pkg05

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestAdd(t *testing.T) {
	// assert equality
	assert.Equal(t, Add(5), 7, "they should be equal")
}

func TestCal(t *testing.T) {
	ass := assert.New(t)
	var tests = []struct {
		input    int
		expected int
	}{
		{2, 4},
		{-1, 1},
		{0, 2},
		{-5, -3},
		{999999997, 999999999},
	}
	for _, test := range tests {
		ass.Equal(Add(test.input), test.expected)
	}
}

2.3.3 mock功能

testify/mock
testfiy/mock

2.3.4 單元測試覆蓋率應用例項

http://github.com/m3db/m3/pull/3525