🤷

Goで一度に複数パッケージのテストをしたい

2022/06/17に公開

今、Node.jsからGoへの移行に挑戦しており、四苦八苦しております。
普通に動くコードを書く分には、私がもともとCやC++を書いていたこともあり、Goの書き方はNode.jsよりも好きなのですが、テストを実行するときに躓いてしまいました。

今、やりたいのは、あるフォルダ、たとえばsrcというフォルダの下にある全てのパッケージのテストを一気にするということです。
例えば、以下のようなファイル構造があったとします。

./src
  |- calculate/
    |- calculate.go
    |- calculate_test.go
    |- go.mod
  |- outLog/
    |- outLog.go
    |- outLog_test.go
    |- go.mod
main.go
go.mod

calculateoutLoggo.modを作らない(moduleの初期化(?)をしない)のであれば

go test -v ./src/...

でテストが可能です。
しかし、main.gocalculateoutLogを使うには、moduleの初期化を行わないといけないので、これでは各モジュールのテストが出来ないのです。

そこで、moduleの初期化を行った上で、全てのmoduleのテストを一気にしてしまう方法を知りたいです。
ググっても、なかなかいい解決方法が出てこなかったので、暗中模索で色々試してみたことを共有したいと思います(結局、強引な解決策しか出てこなかったです…)。

ただし、Goにはinterfaceもありますし、DI(Dependency Injection)も使えそうなので[1]、各モジュールとの依存関係を切って、各モジュールで独立して(責務分離した上で)テストしたほうが良いのだろうなと思うのですが、初心者の私には一気に、そこまで実現できないので、そこまでいく途中経過として共有したいと思います。

各モジュールを初期化(go mod init ~)せずtestしてみる

とても簡単なテストです。ちなみに初期化していないので、main.goでは使えません。

main.go
package main

import "fmt"

func main() {
	fmt.Print("何も出来ない😭")
}
outLog.go
package outlog

import (
	"log"
	"os"
	"time"
)

type OutLog struct {
	To_console bool
	To_file    bool
}

func (ol OutLog) Error(mess string) (rec string, err error) {
	// 現在時刻取得
	t := time.Now()

	// ログのライン
	line := "[" + t.Format(time.RFC3339) + "][Error] - " + mess

	// ファイルへの出力
	if ol.To_file {
		f, err := os.OpenFile("log.txt", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600)
		if err != nil {
			log.Fatal(err)
			return "", err
		}
		defer f.Close()

		_, er := f.WriteString(line + "\n")
		if er != nil {
			log.Fatal(er)
			return "", er
		}
	}

	// コンソールへの出力
	if ol.To_console {
		log.Println(line)
	}

	return line, nil
}
outLog_test.go
package outlog

import "testing"

func TestConsole(t *testing.T) {
	outLog := OutLog{true, false}
	rec, err := outLog.Error("いるかはいるか?🐬")
	if rec == "" || err != nil {
		t.Fatal("エラーの出力失敗")
	}
}
calculate.go
package calculate

func Calculate(n int) int {
	return n * n
}
calculate_test.go
package calculate

import "testing"

func TestCal(t *testing.T) {
	result := Calculate(2)
	if result != 2*2 {
		t.Fatal("calculateの答えが違うよ。")
	}
}

これらのファイルで、main.goがあるディレクトリ上で以下のコマンドをterminalで実行すると

go test -v ./src/...

こんな感じの結果が出ます。

=== RUN   TestCal
--- PASS: TestCal (0.00s)
PASS
ok  	playground/main/src/calculate	(cached)
=== RUN   TestConsole
2022/06/17 14:02:59 [2022-06-17T14:02:59+09:00][Error] - いるかはいるか?🐬
--- PASS: TestConsole (0.00s)
PASS
ok  	playground/main/src/outLog	(cached)

各モジュール(パッケージ)を初期化する

これでは、作ったモジュールを、どこでも使えないので、初期化します。
初期化は、各モジュールのディレクトリで

go mod init [適当な名前]
go mod tidy

を実行すればいいです。
そして、各モジュールにパスを通すために、main.goのディレクトリで

go mod edit -replace [適当な名前]=[モジュールまでの相対パス]
go mod tidy

を実行します。今回は、playground/calculateplayground/outLogという名前にしたので、main.goの中身は以下のようになりました。

main.go
package main

import (
	"fmt"
	"playground/calculate"
	outlog "playground/outLog"
)

func main() {
	ol := outlog.OutLog{To_console: true, To_file: false}
	ol.Error("エラーが出力できた🎉")
	res := calculate.Calculate(3)
	fmt.Printf("計算結果:%d", res)
}

先ほどと同様に以下のコマンドを、main.goがあるディレクトリで実行すると

go test -v ./src/...

今度は、以下の結果が出ます。テストできるパッケージがないと怒られますね。

go: warning: "./src/..." matched no packages
no packages to test

なんとか出来ないかなーと、色々調べた結果、stackoverflowのbcmillsさんの回答[2]を見る限りどうも出来なさそうです…(残念)

この場合の、無理やりな回避方法を、いくつか書きます。

main.goと同じディレクトリにテストファイルを置く

おそらく、要は実行するpackage上でテストを実行すれば問題ないはずなので、以下のようなファイルをmain.goと同じファイルに置けばテスト出来るはずです。

main_test.go
package main

import (
	"playground/calculate"
	outlog "playground/outLog"
	"testing"
)

func TestCal(t *testing.T) {
	result := calculate.Calculate(2)
	if result != 2*2 {
		t.Fatal("calculateの答えが違うよ。")
	}
}

func TestConsole(t *testing.T) {
	outLog := outlog.OutLog{To_console: true, To_file: false}
	rec, err := outLog.Error("いるかはいるか?🐬")
	if rec == "" || err != nil {
		t.Fatal("エラーの出力失敗")
	}
}

calculateoutLogのテストを分けたいのであれば、別ファイルにすれば動きます。
この方法は、同一ディレクトリに沢山テストファイルが貯まってしまうのが嫌ですね。

go test -v [パッケージ名]でテストする

これが、一番、妥協策な気がするのですが、依存するパッケージが多くなるほどコマンドが長くなって辛いです。
例えば、今回、main.gogo.modは以下のようになっていて

go.mod
module playground/main

go 1.18

replace playground/outLog => ./src/outLog

replace playground/calculate => ./src/calculate

require (
	playground/calculate v0.0.0-00010101000000-000000000000
	playground/outLog v0.0.0-00010101000000-000000000000
)

この、playground/calculateplayground/outLogをテストしたいということで、main.goと同じディレクトリで

go test -v playground/calculate playground/outLog

を実行すれば

=== RUN   TestCal
--- PASS: TestCal (0.00s)
PASS
ok  	playground/calculate	(cached)
=== RUN   TestConsole
2022/06/17 15:55:19 [2022-06-17T15:55:19+09:00][Error] - いるかはいるか?🐬
--- PASS: TestConsole (0.00s)
PASS
ok  	playground/outLog	(cached)

という結果が出てきます。
これだと、main.goと同じディレクトリに、テストファイルが溢れることがないので、良いのかなーと思います。

まとめ

すごく、強引に「えいっ!」と解決した感が凄いので、他に良い解決策をご存知の方がいらっしゃいましたら、ご教授いただけると幸いです。
次はDIに挑戦してみます。

おまけ

ここのコードは、以下のGitHubに公開しています(ブランチで分けてます)。

https://github.com/KASHIHARAAkira/go-playground

脚注
  1. 【必須科目 DI】DIの仕組みをGoで実装して理解する / yoshinori_hisakawa https://qiita.com/yoshinori_hisakawa/items/a944115eb77ed9247794 (2022-06-17閲覧) ↩︎

  2. How to run tests from go project root folder? / answer by bcmills https://stackoverflow.com/questions/65758349/how-to-run-tests-from-go-project-root-folder (2022-06-17閲覧) ↩︎

Discussion