0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

過去の自分に教えたい、ソフトウェア品質の心得を投稿しよう by QualityForwardAdvent Calendar 2024

Day 21

Go言語コードの単体テスト go testコマンド, testify/assert, testify/mock,gomockについてのメモ

Last updated at Posted at 2024-12-21

初めに

文系エンジニア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%でした。
Screenshot 2024-12-21 at 16.52.21.png

参考資料

テストについての知見まとめ

単体テストについて

テストケース

コードカバレッジ

ペアワイズ法

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?