# Go 語言單元測試學習筆記
## 關於測試
### 列舉幾種測試
#### 單元測試 (Unit Testing)
單元測試是最小的測試單位,所以執行速度快且可靠,通常由開發人員自行撰寫
單元測試的高度涵蓋是自動化測試 (Test Automation) 的核心
##### 準則
1. 一個測試案例只測一種方法
2. 最小的測試單位
3. 不與外部(包括檔案、資料庫、網路、服務、物件、類別)直接相依
4. 不具備邏輯
5. 測試案例之間相依性為零
##### 排除外部依賴
單元測試關注的是測試程式本身邏輯,所以必須要把外部依賴(Database、File System IO)全部排除。常會使用Mock Data(假資料)來替代從外部依賴獲取資料的流程
##### 涵蓋率建議
1. 確保含有大部分重要邏輯的程式有被涵蓋到即可
2. 需求不確定或經常變動的情況下,寫單元測試的價值可能不高,因為測試也必須經常調整
<br>
#### 整合測試 (Integration Testing)
整合測試比單元測試要高一級,是測試兩個以上的模組之間的交互作用符合預期,對模擬環境的完整度有較高要求
#### 回歸測試 (Regression Testing)
回歸測試是指重複執行既有的全部或部分的相同測試,需要根據需求、時程等問題選擇不同的執行策略
### 測試的開發方法
#### 先開發再測試
略
#### 測試驅動開發(TDD)
1. 寫測試:編寫測試,加入test case (此時test會fail)
2. 寫程式:開始寫code, 目的是要讓 test pass
3. 優化程式碼:並循環以上步驟refactor 你的code, 但test 還是要pass

### 寫測試的好處
* 易於維持程式碼品質
* 確保每次改動或刪除程式碼不會影響到非預期範圍
* 可以透過測試很快了解某功能,包含呼叫情境,傳入的參數及預期的結果值(文件不一定會是最新的,但測試一定是)
* 撰寫過程中可幫助程式開發:降低過度耦合、趨於物件導向原則
# Go寫單元測試
以下將使用幾個範例講解寫測試的方式和介紹一些套件的使用
## testing
使用Go自帶的`testing`測試框架,配合`go test`指令來實現測試
#### 簡單範例([參考](https://medium.com/drunk-wis/golang-%E9%97%9C%E6%96%BC%E5%96%AE%E5%85%83%E6%B8%AC%E8%A9%A6-unit-test-%E6%B8%AC%E4%BB%80%E9%BA%BC-99fcd2e8e27f))
單元測試檔案必須為`_test.go`結尾、測試 func 須符合 `Test...` 的規則
這樣下`go test`指令時才會有效被執行
* 資料夾結構
```
validator // 資料夾名稱
├── validator.go // 功能所在的檔案
└── validator_test.go // 撰寫針對該檔案的測試
```
* 撰寫功能
此功能可用來驗證傳入的字串是否為合法的uuid
```go=
package validator
import "github.com/google/uuid"
func IsValidUUID(u string) bool {
_, err := uuid.Parse(u)
return err == nil
}
```
* 撰寫單元測試
可以利用 `gotests` 工具自動產生單元測試:
游標停在欲測試的func上 -> 鍵盤按`ctl+shift+P`彈出選單 -> 點選「Go:Generate Unit Tests For Function」

將會根據該func自動產生測試的檔案以及基礎的程式碼,再自行補上要測試的case後即可使用
```go=
package validator
import "testing"
func TestIsValidUUID(t *testing.T) {
type args struct {
u string
}
tests := []struct {
name string
args args
want bool
}{
// test cases
{
"caseReturnTrue",
args{
u: "a0a8aea5-cc40-4293-b1f3-c3bc4e53d941",
},
true,
},
{
"caseReturnFalse",
args{
u: "a0a8aea5-cc40-4293-b1f3-",
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValidUUID(tt.args.u); got != tt.want {
t.Errorf("IsValidUUID() = %v, want %v", got, tt.want)
}
})
}
}
```
:::info
呼叫 testing.T 的 Error, Errorf, FailNow, Fatal, FatalIf 方法,說明測試不透過
呼叫 Log 方法用來記錄測試的資訊
:::
當然,也可以自行撰寫測試:
Test Func 建議使用有意義的命名,且須符合`Test{Xxx...}`的規則
```go=
package validator
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsValidUUID_ReturnTrue(t *testing.T) {
rlt := IsValidUUID("a0a8aea5-cc40-4293-b1f3-c3bc4e53d941")
assert.True(t, rlt)
}
func TestIsValidUUID_ReturnFalse(t *testing.T) {
rlt := IsValidUUID("a0a8aea5-cc40-4293-b1f3-c3bc4-")
assert.False(t, rlt)
}
```
> testify [`assert`](https://github.com/stretchr/testify) package
<br>
* 執行測試(所有案例)
*go test -v {filePath}*,沒有加的話就是找當前路徑下的`_test`檔案
預設會執行檔案內的所有測試案例
```shell
$ go test -v ./validator
```
```shell
=== RUN TestIsValidUUID_ReturnTrue
--- PASS: TestIsValidUUID_ReturnTrue (0.00s)
=== RUN TestIsValidUUID_ReturnFalse
--- PASS: TestIsValidUUID_ReturnFalse (0.00s)
PASS
ok go_traning/unit_test/validator 0.358s
```
由於指令中加了`-v`,會顯示測試的詳細過程(不然只會有PASS、ok的那兩行)
<br>
* 執行測試(指定執行某個案例,順便故意改成不通過)
*go test -v -run {testFuncName} {filePath}*,其中 *{testFuncName}* 是為正則表達式
```shell
$ go test -v -run TestIsValidUUID_ReturnTrue ./validator
```
```shell
=== RUN TestIsValidUUID_ReturnTrue
validator_test.go:45:
Error Trace: /Users/esther_lin/Desktop/Go traning/goUnitTest/validator/validator_test.go:45
Error: Should be true
Test: TestIsValidUUID_ReturnTrue
--- FAIL: TestIsValidUUID_ReturnTrue (0.00s)
FAIL
exit status 1
FAIL go_traning/unit_test/validator 0.353s
```
因為是正則表達的關係,如果是執行以下指令:
```shell
$ go test -v -run TestIsValidUUID_Return ./validator
```
*...ReturnTrue* 以及 *...ReturnFalse* 兩個func都會被執行
若要完全指定,使用 *{testFuncName$}* 即可只執行符合該名稱的測試
#### 測試涵蓋率
```shell
$ go test -cover
PASS
coverage: 100.0% of statements
ok go_traning/unit_test/mock/db 0.336s
```
### 踩雷分享
1. 測試時,會掃到同資料夾下的引用,如果裡面有init就會被強制執行(自己的init則不會)
* 舉例
`ex.go`
```go
package testfunc
import (
"package_name/configs" // 引用的package如果裡面有init,在測試時會被執行
"fmt"
)
// 自己的init則不會在測試中被執行到
func init() {
fmt.Println(configs.AppConfig.App.Env)
}
func GetName() string {
return "Hello"
}
```
`ex_test.go`
```go
func TestGetName(t *testing.T) {
tests := []struct {
name string
want string
}{
{
"1",
"Hello",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetName(); got != tt.want {
t.Errorf("GetName() = %v, want %v", got, tt.want)
}
})
}
}
```
`configs/app.go`
```go
func init(){
loadAppConfig()
}
func loadAppConfig(){
AppConfig.App.Env = os.Getenv("Env")
if len(AppConfig.App.Env) == 0{
panic("無效的環境參數")
}
}
```
執行時,會有錯誤
```shell
$ go test -timeout 30s -run ^TestGetName$ crown_connector/testfunc
panic: 無效的環境參數
goroutine 1 [running]:
crown_connector/configs.loadAppConfig()
/file_path/configs/app.go:69 +0x2a9
crown_connector/configs.init.0()
/file_path/configs/app.go:53 +0x17
FAIL crown_connector/testfunc 0.359s
FAIL
```
是因為呼叫到了`/configs` 裡的 `init()`,要避免這個情況,必須把init改為手動呼叫func初始化
<br>
## Mock / Stubs
做出一個模擬的(假的) Function 或 Object 來取代原本程式邏輯內部相對應的 Function 或 Object,如DB回傳、外部API回傳...等
* stub: 驗證目標回傳值,以及驗證目標物件狀態的改變
* mock:驗證目標物件與外部相依介面的互動方式 ex.驗證是否某func被call的次數符合預期
### [gomock](https://github.com/golang/mock) [(Doc)](https://pkg.go.dev/github.com/golang/mock/gomock#pkg-overview)
Go官方提供的套件,可以直接針對檔案產出對應的 mock 檔
但要注意,mock 是針對 interface 產生,檔案中必須要有定義 interface 否則無法使用(對寫法有限制)
#### 簡單範例([參考](https://boy921.medium.com/%E4%BD%BF%E7%94%A8-gomock-%E6%96%BC-unit-test-8ea51b75dd75))
1. 先定義一個 GetNameByIndex 為 interface 的 Method,作為晚點要被替換掉的 func
`get_name.go`
```go=
package db
type DB interface {
GetNameByIndex(index int) string
}
func GetName(db DB, index int) string {
return db.GetNameByIndex(index)
}
```
2. 產生mock檔
如果沒有用過 `mockgen` 的話,要先安裝:
```shell
$ go install github.com/golang/mock/mockgen@v1.6.0
```
要是套件安裝失敗,可以試試這兩個指令
```shell
$ export GOPATH=$HOME/go
$ export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
```
接著使用指令來產生 mock 檔案
```shell
$ mockgen -destination get_name_mock.go -package db -source get_name.go
```
* -source:根據這個檔案去生成 mock
* -destination:要生成 mock 檔的位置,沒寫就直接呈現在terminal裡,不會生成檔案
* -package:mock 檔的 package 名稱,預設為 `mock_` 前綴 source 的 package 名稱
<br>
生成的內容會像這樣:
`get_name_mock.go`
```go=
// Code generated by MockGen. DO NOT EDIT.
// Source: get_name.go
// Package db is a generated GoMock package.
package db
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockDB is a mock of DB interface.
type MockDB struct {
ctrl *gomock.Controller
recorder *MockDBMockRecorder
}
// MockDBMockRecorder is the mock recorder for MockDB.
type MockDBMockRecorder struct {
mock *MockDB
}
// NewMockDB creates a new mock instance.
func NewMockDB(ctrl *gomock.Controller) *MockDB {
mock := &MockDB{ctrl: ctrl}
mock.recorder = &MockDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDB) EXPECT() *MockDBMockRecorder {
return m.recorder
}
// GetNameByIndex mocks base method.
func (m *MockDB) GetNameByIndex(index int) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNameByIndex", index)
ret0, _ := ret[0].(string)
return ret0
}
// GetNameByIndex indicates an expected call of GetNameByIndex.
func (mr *MockDBMockRecorder) GetNameByIndex(index interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNameByIndex", reflect.TypeOf((*MockDB)(nil).GetNameByIndex), index)
}
```
3. 撰寫單元測試
建立 Test func 後,宣告一個 gomock controller
```go
func TestGetName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish() // 最後要關掉它
}
```
接著宣告 *MockDB* 的物件(以下命名為 *m*),將 *ctrl* 帶入
再根據 *GetNameByIndex* 方法去設定 (多個) 帶入的參數以及回傳的數值
`EXPECT()` 代表**期望** *GetNameByIndex* 傳入相當於 `Eq()` 內的參數時
* 回傳 `Return` 中指定的值
* `DoAndReturn` 執行指定的行為後回傳
```go
// _mock.go 產生時定義好的func
// func名稱固定為 NewMock{interfaceName},New出來的物件型態固定為 Mock{interfaceName}
m := NewMockDB(ctrl)
// 設定只要帶入 index = 1 就睡一秒後再回傳 superman
m.
EXPECT().
GetNameByIndex(gomock.Eq(1)). // 由 _mock.go 所實作
DoAndReturn(func(_ int) string { // 此 func 型態必須符合 GetNameByIndex
time.Sleep(1000)
return "superman"
})
// 設定只要帶入 index = 2 就回傳 spiderman 這個字串
m.
EXPECT().
GetNameByIndex(gomock.Eq(2)).
Return("spiderman")
```
:::info
gomock 會假定這個 mock 至少會被調用一次,若無,測試時會報錯「missing call(s)」
欲避免此情況,可以在設定後方加上 `.AnyTimes()`
相關指令可以參考 [Go Mock -gomock- 簡明教程](https://www.readfog.com/a/1641094754324287488)
:::
設定完 mock 後,**要把 *MockDB* 物件帶入要測試的那個 func 裡**
因為它有 implement interface 中的 Method (*GetNameByIndex*),所以能順利取代原本真正要去存取DB的 *GetNameByIndex* [(觀念參考)](https://pjchender.dev/golang/interfaces/)
```go
Convey(testCase.testName, t, func() {
name := GetName(m, testCase.arg) // call 要測試的那個 function
So(name, ShouldEqual, testCase.want)
})
```
> [goconvey](https://github.com/smartystreets/goconvey) package
4. 開測
完整的測試程式碼像這樣:
`get_name_test.go`
```go=
package db
import (
"testing"
"time"
"github.com/golang/mock/gomock"
. "github.com/smartystreets/goconvey/convey"
)
func TestGetName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := NewMockDB(ctrl)
m.
EXPECT().
GetNameByIndex(gomock.Eq(1)).
DoAndReturn(func(_ int) string {
time.Sleep(1000)
return "superman"
})
m.
EXPECT().
GetNameByIndex(gomock.Eq(2)).
Return("spiderman")
testCases := []struct {
testName string
want string
arg int
}{
{
"超人",
"superman",
1,
},
{
"蜘蛛人",
"spiderman",
2,
},
}
for _, testCase := range testCases {
Convey(testCase.testName, t, func() {
name := GetName(m, testCase.arg)
So(name, ShouldEqual, testCase.want)
})
}
}
```
執行結果:
```shell
$ go test -v
=== RUN TestGetName
超人 ✔
1 total assertion
蜘蛛人 ✔
2 total assertions
--- PASS: TestGetName (0.00s)
PASS
ok go_traning/unit_test/mock/db 0.302s
```
# Go專案 單元測試整合CICD
## SonarQube
### 設定
Comma-delimited list of paths to unit test report files. Paths may be absolute or relative to the project root.
```
sonar.go.tests.reportPaths=report.json
sonar.go.coverage.reportPaths=coverage.out
```
> [官方文件](https://docs.sonarqube.org/latest/analyzing-source-code/test-coverage/test-execution-parameters/)、[流程參考](https://community.sonarsource.com/t/sonargo-code-coverage-0/19473/9)
### 產生測試報告
#### go test 方法一 (xml)
1. 產生覆蓋率文件
```shell
$ go test -v ./... -coverprofile=coverage.out
```
`coverage.out`
```out
mode: set
go_traning/unit_test/db/get_name.go:15.51,18.2 1 0
go_traning/unit_test/db/get_name.go:20.39,22.2 1 1
go_traning/unit_test/db/get_name_mock.go:25.49,29.2 3 1
go_traning/unit_test/db/get_name_mock.go:32.47,34.2 1 1
go_traning/unit_test/db/get_name_mock.go:37.51,42.2 4 1
go_traning/unit_test/db/get_name_mock.go:45.78,48.2 2 1
go_traning/unit_test/validator/validator.go:5.33,8.2 2 1
```
2. 轉換為sonar可解讀呈現的檔案格式
需安裝 [`gocov`](https://github.com/axw/gocov) [`gocov-xml`](https://github.com/AlekSi/gocov-xml) 兩個工具
>
```shell
$ gocov convert cover.out
$ gocov-xml > report.xml
```
`report.xml`
<details>
<summary>點擊查看完整XML檔案</summary>
<pre><code>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<coverage line-rate="0.9285714" branch-rate="0" lines-covered="13" lines-valid="14" branches-covered="0" branches-valid="0" complexity="0" version="" timestamp="1676262029871">
<packages>
<package name="go_traning/unit_test/db" line-rate="0.9166667" branch-rate="0" complexity="0" line-count="12" line-hits="11">
<classes>
<class name="-" filename="db/get_name.go" line-rate="1" branch-rate="0" complexity="0" line-count="1" line-hits="1">
<methods>
<method name="GetName" signature="" line-rate="1" branch-rate="0" complexity="0" line-count="0" line-hits="0">
<lines>
<line number="21" hits="1"></line>
</lines>
</method>
</methods>
<lines>
<line number="21" hits="1"></line>
</lines>
</class>
<class name="UserDB" filename="db/get_name.go" line-rate="0" branch-rate="0" complexity="0" line-count="1" line-hits="0">
<methods>
<method name="GetNameByIndex" signature="" line-rate="0" branch-rate="0" complexity="0" line-count="0" line-hits="0">
<lines>
<line number="17" hits="0"></line>
</lines>
</method>
</methods>
<lines>
<line number="17" hits="0"></line>
</lines>
</class>
<class name="-" filename="db/get_name_mock.go" line-rate="1" branch-rate="0" complexity="0" line-count="3" line-hits="3">
<methods>
<method name="NewMockDB" signature="" line-rate="1" branch-rate="0" complexity="0" line-count="0" line-hits="0">
<lines>
<line number="26" hits="1"></line>
<line number="27" hits="1"></line>
<line number="28" hits="1"></line>
</lines>
</method>
</methods>
<lines>
<line number="26" hits="1"></line>
<line number="27" hits="1"></line>
<line number="28" hits="1"></line>
</lines>
</class>
<class name="MockDB" filename="db/get_name_mock.go" line-rate="1" branch-rate="0" complexity="0" line-count="5" line-hits="5">
<methods>
<method name="EXPECT" signature="" line-rate="1" branch-rate="0" complexity="0" line-count="0" line-hits="0">
<lines>
<line number="33" hits="1"></line>
</lines>
</method>
<method name="GetNameByIndex" signature="" line-rate="1" branch-rate="0" complexity="0" line-count="0" line-hits="0">
<lines>
<line number="38" hits="1"></line>
<line number="39" hits="1"></line>
<line number="40" hits="1"></line>
<line number="41" hits="1"></line>
</lines>
</method>
</methods>
<lines>
<line number="33" hits="1"></line>
<line number="38" hits="1"></line>
<line number="39" hits="1"></line>
<line number="40" hits="1"></line>
<line number="41" hits="1"></line>
</lines>
</class>
<class name="MockDBMockRecorder" filename="db/get_name_mock.go" line-rate="1" branch-rate="0" complexity="0" line-count="2" line-hits="2">
<methods>
<method name="GetNameByIndex" signature="" line-rate="1" branch-rate="0" complexity="0" line-count="0" line-hits="0">
<lines>
<line number="46" hits="1"></line>
<line number="47" hits="1"></line>
</lines>
</method>
</methods>
<lines>
<line number="46" hits="1"></line>
<line number="47" hits="1"></line>
</lines>
</class>
</classes>
</package>
<package name="go_traning/unit_test/validator" line-rate="1" branch-rate="0" complexity="0" line-count="2" line-hits="2">
<classes>
<class name="-" filename="validator/validator.go" line-rate="1" branch-rate="0" complexity="0" line-count="2" line-hits="2">
<methods>
<method name="IsValidUUID" signature="" line-rate="1" branch-rate="0" complexity="0" line-count="0" line-hits="0">
<lines>
<line number="6" hits="1"></line>
<line number="7" hits="1"></line>
</lines>
</method>
</methods>
<lines>
<line number="6" hits="1"></line>
<line number="7" hits="1"></line>
</lines>
</class>
</classes>
</package>
</packages>
<sources>
<source>/Users/esther_lin/Desktop/Go traning/goUnitTest</source>
</sources>
</coverage>
</pre></code>
</details>
#### go test 方法二 (json)
```shell
$ go test "./..." -coverprofile="coverage.out" -covermode=count -json > report.json
```
`report.json`
<details>
<summary>點擊查看完整 json檔案</summary>
<pre><code>
{"Time":"2023-02-13T14:27:15.686684+08:00","Action":"output","Package":"go_traning/unit_test","Output":"? \tgo_traning/unit_test\t[no test files]\n"}
{"Time":"2023-02-13T14:27:15.686865+08:00","Action":"skip","Package":"go_traning/unit_test","Elapsed":0}
{"Time":"2023-02-13T14:27:16.484719+08:00","Action":"run","Package":"go_traning/unit_test/db","Test":"TestGetName"}
{"Time":"2023-02-13T14:27:16.484966+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"=== RUN TestGetName\n"}
{"Time":"2023-02-13T14:27:16.48499+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"\n"}
{"Time":"2023-02-13T14:27:16.484999+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":" 超人 \u001b[32m✔\u001b[0m\n"}
{"Time":"2023-02-13T14:27:16.485009+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"\n"}
{"Time":"2023-02-13T14:27:16.485016+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"\u001b[31m\u001b[0m\u001b[33m\u001b[0m\u001b[32m\n"}
{"Time":"2023-02-13T14:27:16.485029+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"1 total assertion\u001b[0m\n"}
{"Time":"2023-02-13T14:27:16.485049+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"\n"}
{"Time":"2023-02-13T14:27:16.485056+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"\n"}
{"Time":"2023-02-13T14:27:16.485063+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":" 蜘蛛人 \u001b[32m✔\u001b[0m\n"}
{"Time":"2023-02-13T14:27:16.48507+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"\n"}
{"Time":"2023-02-13T14:27:16.485078+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"\u001b[31m\u001b[0m\u001b[33m\u001b[0m\u001b[32m\n"}
{"Time":"2023-02-13T14:27:16.48509+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"2 total assertions\u001b[0m\n"}
{"Time":"2023-02-13T14:27:16.485098+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"\n"}
{"Time":"2023-02-13T14:27:16.48515+08:00","Action":"output","Package":"go_traning/unit_test/db","Test":"TestGetName","Output":"--- PASS: TestGetName (0.00s)\n"}
{"Time":"2023-02-13T14:27:16.485162+08:00","Action":"pass","Package":"go_traning/unit_test/db","Test":"TestGetName","Elapsed":0}
{"Time":"2023-02-13T14:27:16.485172+08:00","Action":"output","Package":"go_traning/unit_test/db","Output":"PASS\n"}
{"Time":"2023-02-13T14:27:16.485224+08:00","Action":"output","Package":"go_traning/unit_test/db","Output":"coverage: 91.7% of statements\n"}
{"Time":"2023-02-13T14:27:16.486132+08:00","Action":"output","Package":"go_traning/unit_test/db","Output":"ok \tgo_traning/unit_test/db\t0.317s\tcoverage: 91.7% of statements\n"}
{"Time":"2023-02-13T14:27:16.486194+08:00","Action":"pass","Package":"go_traning/unit_test/db","Elapsed":0.317}
{"Time":"2023-02-13T14:27:16.730648+08:00","Action":"run","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnTrue"}
{"Time":"2023-02-13T14:27:16.730777+08:00","Action":"output","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnTrue","Output":"=== RUN TestIsValidUUID_ReturnTrue\n"}
{"Time":"2023-02-13T14:27:16.730803+08:00","Action":"output","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnTrue","Output":" validator_test.go:45: \n"}
{"Time":"2023-02-13T14:27:16.730818+08:00","Action":"output","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnTrue","Output":" \tError Trace:\t/Users/esther_lin/Desktop/Go traning/goUnitTest/validator/validator_test.go:45\n"}
{"Time":"2023-02-13T14:27:16.730837+08:00","Action":"output","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnTrue","Output":" \tError: \tShould be true\n"}
{"Time":"2023-02-13T14:27:16.73089+08:00","Action":"output","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnTrue","Output":" \tTest: \tTestIsValidUUID_ReturnTrue\n"}
{"Time":"2023-02-13T14:27:16.730913+08:00","Action":"output","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnTrue","Output":"--- FAIL: TestIsValidUUID_ReturnTrue (0.00s)\n"}
{"Time":"2023-02-13T14:27:16.730923+08:00","Action":"fail","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnTrue","Elapsed":0}
{"Time":"2023-02-13T14:27:16.730938+08:00","Action":"run","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnFalse"}
{"Time":"2023-02-13T14:27:16.730949+08:00","Action":"output","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnFalse","Output":"=== RUN TestIsValidUUID_ReturnFalse\n"}
{"Time":"2023-02-13T14:27:16.730961+08:00","Action":"output","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnFalse","Output":"--- PASS: TestIsValidUUID_ReturnFalse (0.00s)\n"}
{"Time":"2023-02-13T14:27:16.730972+08:00","Action":"pass","Package":"go_traning/unit_test/validator","Test":"TestIsValidUUID_ReturnFalse","Elapsed":0}
{"Time":"2023-02-13T14:27:16.730981+08:00","Action":"output","Package":"go_traning/unit_test/validator","Output":"FAIL\n"}
{"Time":"2023-02-13T14:27:16.731117+08:00","Action":"output","Package":"go_traning/unit_test/validator","Output":"coverage: 100.0% of statements\n"}
{"Time":"2023-02-13T14:27:16.732104+08:00","Action":"output","Package":"go_traning/unit_test/validator","Output":"FAIL\tgo_traning/unit_test/validator\t0.509s\n"}
{"Time":"2023-02-13T14:27:16.732145+08:00","Action":"fail","Package":"go_traning/unit_test/validator","Elapsed":0.509}
</pre></code>
</details>
#### goconvey
適合本地查看測試狀態,需使用該套件寫斷言
1. 安裝
```shell
$ go install github.com/smartystreets/goconvey
```
2. 下此指令後,會自動以瀏覽器開啟介面(http://localhost:8080)
```shell
$ goconvey
```
* 成功的

* 失敗的

3. 從terminal中可以看到,只要程式有異動就會持續刷新測試結果
```shell
2023/02/13 15:01:53 integration.go:122: File system state modified, publishing current folders... 10054679803 10054679830
2023/02/13 15:01:53 goconvey.go:159: Received request from watcher to execute tests...
2023/02/13 15:01:54 executor.go:69: Executor status: 'executing'
2023/02/13 15:01:54 coordinator.go:46: Executing concurrent tests: go_traning/unit_test
2023/02/13 15:01:54 coordinator.go:46: Executing concurrent tests: go_traning/unit_test/db
2023/02/13 15:01:54 coordinator.go:46: Executing concurrent tests: go_traning/unit_test/validator
2023/02/13 15:01:55 shell.go:89: Coverage output: ? go_traning/unit_test [no test files]
2023/02/13 15:01:55 shell.go:91: Run without coverage
2023/02/13 15:01:56 parser.go:24: [passed]: go_traning/unit_test/validator
2023/02/13 15:01:56 parser.go:24: [no test files]: go_traning/unit_test
2023/02/13 15:01:56 parser.go:24: [passed]: go_traning/unit_test/db
2023/02/13 15:01:56 executor.go:69: Executor status: 'idle'
```
# Go專案 測試實例
## 踩雷
### 小心外部引用的初始化func
#### 問題描述
執行測試時,如果被呼叫的那個 func 檔案所在位置的路徑下(包含自身),有 import 內部套件比如DB方法、自訂義logger等等,且當中含有 `init()` 函式;該函式會於測試中被執行,導致測試會因為找不到連線物件或環境變數而執行失敗,**即使被測試的func本身並沒呼叫相關方法也一樣**
#### 範例
現在有個 GetExample 非常單純的功能要被測試,但同個檔案內有其他的func會去call DB,所以 **import 了 DB**
`internal/odds.go`
```go
import (
"fmt"
"go_traning/unit_test/db"
)
// 要被測試的範例
func GetExample() bool {
return true
}
// 其他不相干的 func
func GetOdds(eventInfo EventInfo) (result []Market, err error) {
// 從DB取得原始資料
rawOdds, _ := db.GetOddsByEventID(eventInfo.SourceEventID)
// 功能邏輯
// ...
```
DB 資料夾下有 `init()` 作用於取環境參數後去進行連線,若取不到服務會 panic
`db/init.go`
```go
package db
import "os"
func init() {
if os.Getenv("DB_HOST") == "" {
panic("缺少環境參數")
}
// DB連線邏輯
// ...
}
```
幫 GetExample 寫了一個測試案例,內容也很單純
`internal/odds_test.go`
```go
func TestGetExample(t *testing.T) {
tests := []struct {
name string
want bool
}{
{
"true",
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetExample(); got != tt.want {
t.Errorf("GetExample() = %v, want %v", got, tt.want)
}
})
}
}
```
執行後...Opps! 怎麼 run 不起來
```shell
panic: 缺少環境參數
goroutine 1 [running]:
go_traning/unit_test/db.init.0()
/Users/esther_lin/Desktop/Go traning/goUnitTest/db/init.go:7 +0x47
FAIL go_traning/unit_test/internal/odds_before 0.291s
FAIL
```
#### 解決辦法
1. 避免使用 `init()`,改為定義好 func 讓 `main.go` 去 call
2. 將功能拆細並收攏於不同的 package 內,減少引用混雜的機會
## To be continue...
## 參考資料
[TDD (Test-Driven Development) 測試驅動開發(入門篇)](https://medium.com/%E6%88%91%E6%83%B3%E8%A6%81%E8%AE%8A%E5%BC%B7/tdd-test-driven-development-%E6%B8%AC%E8%A9%A6%E9%A9%85%E5%8B%95%E9%96%8B%E7%99%BC-%E5%85%A5%E9%96%80%E7%AF%87-e3f6f15c6651)
[30天快速上手TDD系列 第 2 篇](https://ithelp.ithome.com.tw/articles/10102264)
[為什麼程式需要單元測試? - 概念篇](https://www.gss.com.tw/blog/why-program-need-unit-test-intro)
[[C#][Unit Test] 04. Mock (仿製資料)](https://progressbar.tw/posts/34)
[go test命令(Go语言测试命令)完全攻略](http://c.biancheng.net/view/124.html)
[A Quick Way to Generate Go Tests in Visual Studio Code](https://betterprogramming.pub/a-quick-way-to-generate-go-tests-in-visual-studio-code-b7c675b88dac)
[Golang Unit Test(二)](https://medium.com/skyler-record/golang-unit-test-%E4%BA%8C-ce1e4bb329a5)
[Golang Test - 單元測試、Mock與http handler 測試](https://yuanchieh.page/posts/2021/2021-03-18-golang-test/)
[Go Mock -gomock- 簡明教程](https://www.readfog.com/a/1641094754324287488)
[How to write stronger unit tests with a custom go-mock matcher](https://dev.to/techschoolguru/how-to-write-stronger-unit-tests-with-a-custom-go-mock-matcher-55pc)
[Testing with GoMock: A Tutorial](https://gist.github.com/thiagozs/4276432d12c2e5b152ea15b3f8b0012e)
[跟煎魚學Go - 1.4 使用 Gomock 进行单元测试](https://eddycjy.gitbook.io/golang/di-1-ke-za-tan/gomock)
[SonarQube and code coverage](https://community.sonarsource.com/t/sonargo-code-coverage-0/19473/9)
[Go单测从零到溜系列5—goconvey的使用
](https://www.liwenzhou.com/posts/Go/unit-test-5/)