# Go 的測試到底要放哪?

在 Line 群上有至少多位網友問過類似下面的問題.

1. 那各位在比較正式的專案還會用 service locator / ioc container 先把依賴或依賴的工廠集中起來嗎? (所以測試的時候 要特別去引用 service locator 把依賴從 測試取出?)
    
2. xxx\_test.go 的 package name 會加上 *test 嗎? (目前觀察到的差異是加上* test 會 access 不到小寫開頭的變數)
    

常見的有

1. 跟待測試模組同資料夾, 檔名是`_test.go`, 且跟待測試模組同package, 稱為`內部測試文件`.
    
2. 跟待測試模組同資料夾, 檔名是`_test.go`, 且跟待測試模組不同package, 他的package 名稱會是 \``xxx_test,` 稱為 `外部測試文件`.
    
3. 通常在根目錄有個獨立的 `test` 資料夾.
    

## go list

當使用 Go 語言進行開發時，測試是一個不可或缺的部分。[`go list`](https://pkg.go.dev/cmd/go/internal/list) 命令是一個強大的工具，可以用來檢視和操作 Go 模組的資訊。

`go list` 通常會搭配 `-f` 一起使用, `-f` 指定輸出格式，通常使用 [Go 模板語法](https://pkg.go.dev/text/template)。.

| Term | 回傳類型 | 描述 |
| --- | --- | --- |
| ImportPath | string | 當前 package 下套件的導入路徑 |
| TestGoFiles | \[\]string | 列舉當前package中的內部測試文件（同package名稱，`_test.go` 結尾）。 |
| XTestGoFiles | \[\]string | 當前package中的外部測試文件（不同packge名，`_test.go` 結尾） |
| TestImports | \[\]string | 內部測試文件中導入的所有package的導入路徑。 |
| XTestImports | \[\]string | 外部測試文件中導入的所有package的導入路徑。 |

有以上這些我就能在有 go.mod 的專案中來快速查找有沒有測試文件, 分別又是內部或是外部測試文件.

像是我在 opentelemetry-go-contrib 專案中分別輸入以下兩個指令, 會出現不同的查找結果.

`go list ./…` 這個命令將列出當前目錄及所有子目錄中的所有包的導入路徑。

`go list -f '{{.ImportPath}}: {{.XTestGoFiles}}' ./...` 列出所有外部測試文件 (XTestGoFiles)。

`go list -f '{{.ImportPath}}: {{.TestGoFiles}}' ./...` 列出所有內部測試文件 (TestGoFiles)。

```bash
> opentelemetry-go-contrib/detectors/aws/ecs
go list ./...
# go.opentelemetry.io/contrib/detectors/aws/ecs
# go.opentelemetry.io/contrib/detectors/aws/ecs/test

go list -f '{{.ImportPath}}: {{.XTestGoFiles}}' ./...
# go.opentelemetry.io/contrib/detectors/aws/ecs: [version_test.go]
# go.opentelemetry.io/contrib/detectors/aws/ecs/test: []

go list -f '{{.ImportPath}}: {{.TestGoFiles}}' ./...
# go.opentelemetry.io/contrib/detectors/aws/ecs: [ecs_test.go]
# go.opentelemetry.io/contrib/detectors/aws/ecs/test: [ecs_test.go]
```

當然也能自由組合, 例如 `go list -f '{{.ImportPath}}: TestGoFiles: {{.TestGoFiles}} XTestGoFiles: {{.XTestGoFiles}} Imports: {{.Imports}} Deps: {{.Deps}}' ./...`, 列出每個 package 的內部測試文件、外部測試文件和所有依賴。 能自己玩看看。

---

在 [go testing 套件](https://pkg.go.dev/testing)的一開始說明有一段, 這裡就在講述**內部測試文件**與**外部測試文件**.

> To write a new test suite, create a file that contains the TestXxx functions as described here, and give that file a name ending in "\_test.go". The file will be excluded from regular package builds but will be included when the "go test" command is run.
> 
> The test file can be in the same package as the one being tested, or in a corresponding package with the suffix "\_test".
> 
> If the test file is in the same package, it may refer to unexported identifiers within the package.
> 
> If the file is in a separate "\_test" package, the package being tested must be imported explicitly and only its exported identifiers may be used. This is known as "black box" testing.

## 內部測試文件

跟待測試模組同資料夾, 檔名是`_test.go`, 且跟待測試模組同package。也就是說.

```bash
tree
.
├── go.mod
├── go.sum
├── mux.go
├── mux_test.go


cat mux_test.go
> package mux
```

雖然兩個是同一個package, 但此時你能做實驗, 在 `exporter_test.go`裡面宣告的public 成員其實在 非 \_test.go 的一般模組中是無法被引用的. 這點在 [go 官方網站講到 build](https://pkg.go.dev/cmd/go@master#hdr-Test_packages) 時也有提到.

> When compiling packages, build ignores files that end in '\_test.go'.

## 外部測試文件

待補

## 獨立 test 資料夾

我一樣以 `opentelemetry-go-contrib` 專案來做說明.

```bash
cd opentelemetry-go-contrib/instrumentation/github.com/gorilla/mux/otelmux
tree
.
├── go.mod
├── go.sum
├── mux.go
├── mux_test.go
├── test
│   ├── doc.go
│   ├── go.mod
│   ├── go.sum
│   ├── mux_test.go
│   ├── version.go
│   └── version_test.go
├── version.go
└── version_test.go
```

在 Go 專案中，測試通常與被測試的程式碼放在同一個模組中，使用相同或不同的 package 名稱來進行內部測試或外部測試。然而，有時候為了特定需求，我們可能會將測試代碼放在一個**獨立的模組**中。這意味著測試程式碼有自己獨立的 `go.mod` 文件，並且可以擁有不同的依賴，這與主模組的依賴隔離開來。

### 為什麼使用獨立測試模組？

1. **隔離依賴**：
    
    * **避免傳遞性依賴**：主模組（例如 `otelmux` 的主要模組）不需要依賴預設的 SDK，這樣使用 `otelmux` 的用戶不會因為主模組的依賴而被迫引入預設 SDK。
        
    * **獨立管理測試依賴**：測試模組可以單獨管理其依賴，如預設 SDK，這些依賴僅在測試時使用，不會影響主模組的依賴樹。
        
2. **支持更複雜的測試場景**：
    
    * **整合測試**：獨立模組可以更容易地進行整合測試，因為它們可以引入更多的依賴和工具來模擬真實的使用場景。
        
    * **專門的測試工具**：可以引入專門用於測試的工具套件（如 `testutil`），而不會干擾主模組的依賴。
        
3. **提升模組的可維護性和靈活性**：
    
    * **減少主模組的依賴**：保持主模組的依賴輕量，提升其可維護性和靈活性。
        
    * **獨立更新測試程式碼**：測試模組可以獨立更新和升級，而不會影響主模組的穩定性。
        
4. **避免循環導入（Import Cycle）**：
    
    * 如您所提到的，循環導入會導致 Go 編譯失敗。獨立測試模組可以避免這種情況，因為測試模組不需要反向引用主模組。
        
    * 測試模組可以作為獨立的實體存在，避免因測試需求而導致的循環依賴問題。
        
5. **\[缺\] 增加項目結構的複雜性**：
    
    * 需要管理多個 `go.mod` 文件和模組，增加了項目的結構複雜性。
        
    * 對於新手來說，可能需要花更多時間來理解和管理多模組項目。
        
6. **\[缺\] 可能的重複程式碼**：
    
    * 在某些情況下，主模組和測試模組可能需要共享一些程式碼，這可能導致程式碼重複或需要額外的程式碼共享機制。
        
7. **\[缺\] 需要額外的配置和維護**：
    
    * 需要為獨立測試模組設定與配置 CI/CD pipeline、測試覆蓋率報告等，增加了維護的工作量。
        

## 參考資料

[Golang - test package 在內跟在外的差別](https://blog.kennycoder.io/2022/09/10/Golang-test-package-%E5%9C%A8%E5%85%A7%E8%B7%9F%E5%9C%A8%E5%A4%96%E7%9A%84%E5%B7%AE%E5%88%A5/)
