原文鏈接: Go 語言單元測試實踐

什麼是軟件測試?

軟件測試是一個過程,該過程對軟件(計算機程序)進行各種操作來發現軟件錯誤。

爲什麼要進行軟件測試?

進行軟件測試可以幫助我們驗證軟件的各種功能正常,保證軟件的正常工作從而提高軟件質量。 並且在實踐中已被證明是頗有成效的

測試驅動開發的由來:

一個從大量實踐中得出的結論:人們發現在軟件開發週期中,軟件錯誤每進入到下一個階段要修正它所付出的時間和人力會出人意表的翻上十倍。所以更早地進行軟件測試可以更早地發現軟件錯誤,從而大大減少後期修正的成本。後來又有人提出了 測試驅動開發(TDD: Test-driven development) ,主體思想就是 先編寫測試程序,再實現程序功能

下面就來介紹如何在 Go 語言中進行軟件測試中較爲重要的一環:單元測試。

Go 語言單元測試實踐

單元測試簡介

單元測試就是針對程序最小單元的測試。

最小單元在過程化編程中指的是 函數 ;在面向對象編程中指的是 方法

Go 語言對軟件測試的支持

go tool

規定(必須遵循的條例):

  • 只有以 _test.go 結尾的文件纔會被視作是測試文件。如: swap.go
  • 測試函數命名必須以 Test 開頭,而後緊接着的第一個字母必須大寫。如: TestSwap
  • 測試函數必須傳入一個指向 testing.Ttesting.B (基準測試)的指針作爲參數。如: func TestSwap(t *testing.T) ,且沒有返回值。

建議(最佳方法實踐條例):

  • 將測試文件和要測試的代碼放在同一個包(Go 語言組織也建議包名和文件所在目錄名相同)。也就是說將這兩個文件的 package xxx 寫成相同的。
  • 將測試文件命名爲 要測試的代碼文件名_test.go 。如: util.go 的測試文件名應爲 util_test.go
  • 將測試函數命名爲 Test要測試的函數名稱 。如: Swap 函數的測試函數命名應爲 TestSwap ,這個函數應只包含測試 Swap 函數的內容。

Go 語言單元測試實踐

示例函數說明

這裏有一個用 Go 語言編寫的 Swap 函數(交換切片中的兩個值):

swap.go
swap
swap_test.go
swap

這兩個文件都放在 $GOPATH/src/swap/ 目錄下。

  • 測試整個包: go test swapswap 爲基於 $GOPATH/src/ 的相對目錄。
  • 測試單個測試函數: go test swap -run TestSwapswap 同樣爲相對目錄, TestSwap 爲測試函數名。

下面會展示如何爲這個 Swap 函數 編寫單元測試、將單元測試改寫成表驅動測試的形式並顯示代碼的測試覆蓋率

示例函數代碼

package swap

import (
	"errors"
)

// Swap exchanges s[i] and s[j].
func Swap(s []interface{}, i, j int) error {
	if s == nil {
		return errors.New("slice can't be nil")
	}
	if (i < 0 || i >= len(s)) || (j < 0 || j >= len(s)) {
		return errors.New("illegal index")
	}

	s[i], s[j] = s[j], s[i]

	return nil
}

// IsSameSlice determines two slice is it the same.
func IsSameSlice(a, b []interface{}) bool {
	if len(a) != len(b) {
		return false
	}
	if (a == nil) != (b == nil) {
		return false
	}

	for i, v := range a {
		if b[i] != v {
			return false
		}
	}

	return true
}
複製代碼

單元測試示例

package swap

import (
	"testing"
)

func TestSwap(t *testing.T) {
	input1 := []interface{}{1, 2}
	i1 := 0
	j1 := 1
	want1 := []interface{}{2, 1}
	if err := Swap(input1, i1, j1); err != nil {
		t.Error(err)
	}

	if !IsSameSlice(input1, want1) {
		t.Errorf("got %v, want %v", input1, want1)
	}

	input2 := []interface{}{1, 'a', "aa"}
	i2 := 0
	j2 := 2
	want2 := []interface{}{"aa", 'a', 1}
	if err := Swap(input2, i2, j2); err != nil {
		t.Error(err)
	}

	if !IsSameSlice(input2, want2) {
		t.Errorf("got %v, want %v", input2, want2)
	}
}
複製代碼

這段代碼就是爲 Swap 編寫的簡單單元測試,可以看出,如果測試數據變多,代碼就會有很多 冗餘 。這種問題的一個有效地解決方法就是 用表驅動測試來實現單元測試

表驅動測試示例

表驅動測試是單元測試的一種形式,通過把測試條件都寫在一張表裏,就可以動態地添加測試數據而不用改動太多代碼。

package swap

import (
	"testing"
)

func TestSwap(t *testing.T) {
	tests := []struct {
		input []interface{}
		i     int
		j     int
		want  []interface{}
	}{
		{[]interface{}{1, 2}, 0, 1, []interface{}{2, 1}},
		{[]interface{}{1, 'a', "aa"}, 0, 2, []interface{}{"aa", 'a', 1}},
	}
	for i, tt := range tests {
		if err := Swap(tt.input, tt.i, tt.j); err != nil {
			t.Error(err)
		}

		if !IsSameSlice(tt.input, tt.want) {
			t.Errorf("%v. got %v, want %v", i, tt.input, tt.want)
		}
	}
}
複製代碼

查看測試覆蓋率

測試覆蓋率就是測試運行到的被測試代碼的代碼數目。其中以 語句的覆蓋率 最爲簡單和廣泛,語句的覆蓋率指的是 在測試中至少被運行一次的代碼佔總代碼數的比例

使用 go tool cover 命令來查看有關測試覆蓋率命令行的幫助。

注意:首先必須保證測試是能夠通過的。

go test -cover=true swap
go test -cover=true swap -run TestSwap

-cover=true 選項會開啓覆蓋率說明。

生成 HTML 報告

前面展示的都是命令行下的報告,不夠直觀。

其實 go tool cover 支持更友好的輸出,如:HTML 報告。

go test -cover=true swap -coverprofile=out.out 將在當前目錄生成覆蓋率數據。

配合 go tool cover -html=out.out 在瀏覽器中打開 HTML 報告。

或者使用 go tool cover -html=out.out -o=out.html 生成 HTML 文件。

Go 語言測試覆蓋率 HTML 報告

第三方測試框架

一般情況下,Go 語言標準庫對測試的支持已足夠強大。但是也有許多第三方的庫提供了各種各樣的功能。這裏只介紹兩個使用廣泛的庫和其主要功能。

使用 testify 包簡化測試代碼

安裝: go get -u github.com/stretchr/testify

testify 提供 assertmockhttp 三個包進行更多樣的測試。下面用其中的 assert 包改寫前面的例子。

package swap

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

func TestSwap(t *testing.T) {
	input1 := []interface{}{1, 2}
	i1 := 0
	j1 := 1
	want1 := []interface{}{2, 1}
	if assert.Nil(t, Swap(input1, i1, j1)) {
		t.FailNow()
	}

	assert.Equal(t, input1, want1)

	input2 := []interface{}{1, 'a', "aa"}
	i2 := 0
	j2 := 2
	want2 := []interface{}{"aa", 'a', 1}
	if assert.Nil(t, Swap(input2, i2, j2)) {
		t.FailNow()
	}

	assert.Equal(t, input2, want2)
}
複製代碼

使用 gotests 生成測試代碼

安裝: go get -u github.com/cweill/gotests

gotests 可以爲程序生成表驅動測試,在生成之後再添加測試數據即可完成測試代碼的編寫。

使用命令: gotests -all -w go/src/swap/swap.go 即可生成關於 swap.go 文件中所有函數的表驅動測試形式的單元測試,包含在與 swap.go 同目錄下的 swap_test.go 文件中。

package swap

import "testing"

func TestSwap(t *testing.T) {
	type args struct {
		s []interface{}
		i int
		j int
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := Swap(tt.args.s, tt.args.i, tt.args.j); (err != nil) != tt.wantErr {
				t.Errorf("Swap() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}
複製代碼

上面就是運行命令之後生成的代碼。在 TODO 中添加測試數據即可。

package swap

import "testing"

func TestSwap(t *testing.T) {
	type args struct {
		s []interface{}
		i int
		j int
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
	}{
		{"1", args{[]interface{}{1, 2}, 0, 1}, false},
		{"2", args{[]interface{}{1, 'a', "aa"}, 0, 2}, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if err := Swap(tt.args.s, tt.args.i, tt.args.j); (err != nil) != tt.wantErr {
				t.Errorf("Swap() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}
複製代碼

可以看出這裏並沒有測試交換後的值是否正確,說明 gotests 的代碼會根據返回值生成,也說明了 gotests 對某些函數來說是不適用的或者說是需要手動修改的。

相關文章