初めに
文系エンジニア1年目がGoで開発することになったのですが、社内にドキュメントが見つからずGo言語の単体テストのお作法がわからない…
ということでネットの情報をまとめて今後に活かそうと思います。そして未来の自分用にこの記事を残したいとも思ってます。
つなない文章ではありますが誰かの役にたれてば嬉しいです。
Part 1 Go言語 単体テストについて
Golangの標準パッケージtestingを使用すると、単体テストを書くことができます。
単体テスト、テストケース、カバレッジなどの知識が前提となっておりますので、わからない方はテストについての知見まとめをご覧ください。
命名はファイル名に_test
をつけるがお決まりです。
ディレクトリ構成はこのようになっております。
interactor/
├── example.go
└── example_test.go
次にassertとmockを紹介します。
はじめに結論を述べると下記の役割を持ちます。
- testify/assert: テスト中に使われるアサーション関数を提供し、テストの可読性と簡潔さを向上させるライブラリ
- testify/mock: モックオブジェクトを作成して依存関係をシミュレートし、テスト対象を分離するためのモッキングライブラリ
Part 1.1 testify/assert
testify/assert は、テスト中に条件を確認するためのアサーションライブラリです。標準の testing パッケージを拡張して、テストコードをより簡潔に、読みやすくすることを目指しています。
主な機能
- 単純な assert.Equal から assert.NotNil、assert.Errorなど、多くのアサーション関数を提供
- テストが失敗した場合に、どのアサーションが失敗したのかを明確にします
- テストコードの簡潔さと可読性向上を重視
使用例
package interactor
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExample(t *testing.T) {
value1 := 10
value2 := 10
// 値が等しいかどうかを確認
assert.Equal(t, value1, value2, "The two values should be equal")
var obj interface{}
// 値がNilであるか確認
assert.Nil(t, obj, "The object should be nil")
}
Part 1.2 testify/mock
testify/mock は、モックオブジェクトを簡単に作成するためのモッキングライブラリです。依存関係をモックし、テスト対象のコードをより分離し、外部要因に影響されないテストを可能にします。
なお後述するgomockを使用した方が個人的には良かったなと思うので、そちらもご参照ください。
主な機能
- インターフェースの実装をモックすることで、依存関係を容易にシミュレート
- モックオブジェクトのメソッド呼び出しの期待値を設定し、それが期待通りに呼ばれたかを検証
- 呼び出し順序や呼び出し回数などの細かい検証が可能
使用例
package interactor
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
"""
インターフェースの定義。
なおこれはテスト対象で使用されているリポジトリインターフェースや
プレゼンターのインターフェースの為本来はここに記述されていない
"""
type SampleInterface interface {
GetValue(int) (string, error)
}
// MockSample は SampleInterface のモック
type MockSample struct {
mock.Mock
}
func (m *MockSample) GetValue(id int) (string, error) {
args := m.Called(id)
// エラー条件のチェック
if id < 0 {
return "", errors.New("invalid id")
}
return args.String(0), args.Error(1)
}
// テスト
func TestExample(t *testing.T) {
mockSample := new(MockSample)
t.Run("Positive Test", func(t *testing.T) {
// モックの設定
mockSample.On("GetValue", 1).Return("mocked value", nil)
// テストの実行
result, err := mockSample.GetValue(1)
// 結果の検証
assert.NoError(t, err)
assert.Equal(t, "mocked value", result)
mockSample.AssertCalled(t, "GetValue", 1)
mockSample.AssertExpectations(t)
})
t.Run("Negative Test", func(t *testing.T) {
// モックの設定
mockSample.On("GetValue", -1).Return("", errors.New("invalid id"))
// テストの実行
result, err := mockSample.GetValue(-1)
// 結果の検証
assert.Error(t, err)
assert.Equal(t, "invalid id", err.Error())
assert.Equal(t, "", result)
mockSample.AssertCalled(t, "GetValue", -1)
mockSample.AssertExpectations(t)
})
}
part 1.3 gomock
testify/mockと比べた良いところ
1.より詳細なコントロール
GoMockは、メソッドの呼び出し順序や特定の呼び出しを期待する設定を詳細に制御できます。testify/mock
と比べて、複雑なモックシナリオに対する柔軟な設定が可能です。
2.強力なモックジェネレーター
GoMockのmockgen
ツールは、インターフェースから自動的にモックコードを生成し、高い自動化と一貫性を維持することができます。
特にモックの自動化に関してはモックを生成したいファイルにコマンドを書き、go generate
コマンドを打つことで一括生成、変更が行われます。
以前使われていたgomockはGoogleが保守していましたが、現在はUberがforkメンテしているそうです。
ソース:https://x.com/Kiyo_Karl2/status/1674190438708973568
こちらがリポジトリです。https://github.com/uber-go/mock
1.GoMock のインストール:
$ go install go.uber.org/mock/mockgen@latest
2.モックの生成:
user_repository.go に基づいてモックを生成します。以下のコマンドをターミナルで実行します。
$ mockgen -source=test_repository.go -destination=../mock/mock_test_repository.go -package=mocks
またはモックを生成したいファイルに下記のコメントを追記してください。
//go:generate mockgen -source=test_repository.go -destination=../mock/mock_test_repository.go -package=mocks
その後ルートディレクトリにてモックが生成されます。
$ go generate
モックの生成例
mockgen コマンドにより生成されたモックの例です。
// Code generated by MockGen. DO NOT EDIT.
// Source: user_repository.go
package mock
import (
"github.com/golang/mock/gomock"
"reflect"
)
type MockSampleInterface struct {
ctrl *gomock.Controller
recorder *MockSampleInterfaceMockRecorder
}
type MockSampleInterfaceMockRecorder struct {
mock *MockSampleInterface
}
func NewMockSampleInterface(ctrl *gomock.Controller) *MockSampleInterface {
mock := &MockSampleInterface{ctrl: ctrl}
mock.recorder = &MockSampleInterfaceMockRecorder{mock}
return mock
}
func (m *MockSampleInterface) EXPECT() *MockSampleInterfaceMockRecorder {
return m.recorder
}
func (m *MockSampleInterface) GetValue(id int) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetValue", id)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
func (mr *MockSampleInterfaceMockRecorder) GetValue(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValue", reflect.TypeOf((*MockSampleInterface)(nil).GetValue), id)
}
テストコード
生成されたモックを使ったテストコードの例です。TestExample
関数内でポジティブケースとネガティブケースの両方をテストします。
package interactor
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/yourmodule/mock" // <-- ここは実際のモック生成先に合わせて変更してください
)
func TestExample(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSample := mock.NewMockSampleInterface(ctrl) // 生成されたモックを使用
t.Run("Positive Test", func(t *testing.T) {
// モックの設定
mockSample.EXPECT().GetValue(1).Return("mocked value", nil)
// テストの実行
result, err := mockSample.GetValue(1)
// 結果の検証
assert.NoError(t, err)
assert.Equal(t, "mocked value", result)
})
t.Run("Negative Test", func(t *testing.T) {
// モックの設定
mockSample.EXPECT().GetValue(-1).Return("", errors.New("invalid id"))
// テストの実行
result, err := mockSample.GetValue(-1)
// 結果の検証
assert.Error(t, err)
assert.Equal(t, "invalid id", err.Error())
assert.Equal(t, "", result)
})
}
Part 2 go test コマンド
go test コマンドは、Go のテストコードを実行するためのコマンドです。基本的な使い方から、様々なオプション、テストカバレッジの取得まで、詳しく解説していきます。
基本的な使い方
go test コマンドを実行すると、カレントディレクトリにある *_test.go ファイルをすべて実行します。
$ go test # カレントディレクトリのパッケージをテスト
$ go test ./pkg/hogehoge/example # 特定のパッケージをテスト
$ go test ./... # このコマンドは、カレントディレクトリ以下のすべてのパッケージに対してテスト
2.1 オプション
go test コマンドには、様々なオプションがあります。
オプション | 説明 |
---|---|
-v |
テストの詳細を表示します。 |
-run |
特定のテスト関数またはサブテストを実行します。正規表現を使用できます。 |
-bench |
ベンチマークテストを実行します。 |
-cover |
テストカバレッジを計測します。 |
-coverprofile |
テストカバレッジのプロファイルをファイルに出力します。 |
-race |
データ競合を検出するためのテストを実行します。 |
-failfast |
最初にテストが失敗したら、テストを中止します。 |
-short |
テスト時間を短縮するためのショートテストを実行します。 |
-timeout |
テストの実行タイムアウトを設定します。 |
-cpu |
テストに使用する CPU コア数を指定します。 |
-parallel |
並列に実行するテスト数を指定します。 |
2.2 テストカバレッジ
-cover オプションを使用すると、テストカバレッジを計測できます。
# テストカバレッジを表示
$ go test -cover
ok github.com/your-module/your-package 0.254s coverage: 87.5% of statements
# カバレッジプロファイルを coverage.out に出力
# 出力されたプロファイルは、go tool cover コマンドで分析できます。
$ go test -coverprofile=coverage.out
# coverage.out を HTML で表示
$ go tool cover -html=coverage.out
例)coverage.out
mode: set
github.com/your-module/your-package/your_file.go:10.2,12.5 1 0
github.com/your-module/your-package/your_file.go:15.33,17.2 1 1
1 行目: mode: set は、ステートメントが少なくとも 1 回実行されたかどうかを計測するモードです。
2 行目: your_file.go の 10 行目 2 文字目から 12 行目 5 文字目までが 1 回実行され、0 回実行されなかったことを示します。
3 行目: your_file.go の 15 行目 33 文字目から 17 行目 2 文字目までが 1 回実行され、1 回実行されなかったことを示します。
例)HTML
この時の単体テストケースではカバレッジ率は80%でした。
参考資料
- https://qiita.com/hiroyky/items/4a9be463e752d5c0c41c
- https://zenn.dev/shunpay/articles/839b2805d6fd11
- https://qiita.com/ryu3/items/a2e39157bf1d55be149f
- https://qiita.com/takehanKosuke/items/4342ca544d205fb36eb0
- https://gihyo.jp/article/2023/03/tukinami-go-05
- https://zenn.dev/sanpo_shiho/articles/01da627ead98f5
テストについての知見まとめ
単体テストについて
テストケース
コードカバレッジ
- コードカバレッジとは?
- ホワイトボックステストにおけるカバレッジ(C0/C1/C2/MCC)について
- テストカバレッジ100%を追求しても品質は高くならない理由と推奨されるカバレッジの目標値について
- コードカバレッジを見つつユニットテストを書く
ペアワイズ法