📝

Go + Clean Architecture × Testify/mock でユースケース層のテストを書く方法

に公開

最近、Go言語で「Clean Architecture」構成のTODOアプリを個人開発しています。

先ほどXでこんなこと呟いていた
https://twitter.com/applexnino/status/1922462753148538957

完全に主観ですが、個人開発では処理の実装や見た目の実装が最優先になって、
テストコードを書いていない人が多い印象です(実際、自分もそうでした)。

ただ、実務ではテストコードを書くことが前提の開発も多く、
今回の開発では「ユースケース層のテストをちゃんと書いてみよう!」と思い立ち、Testify/mockを使ってテストを導入しました。

GoでClean Architectureを取り入れているけど…

  • Usecase層のテスト、どう書けばいいの?
  • DBアクセスなしでテストできるようにしたい
  • mockの使い方がまだピンとこない

そんな人向けに、Testify/mock を使ったユースケース層のテスト方法を紹介します!

  • テストフレームワーク:Testify(assert / mock
  • 構成:Usecase層 + RepositoryのInterface + Mock

🎯 検証対象の構成(例:TODO機能)

TodoUsecase
├── CreateTodo(todo *Todo)
├── GetByID(id int)
├── Update(todoID, todo)
├── Delete(todoID, customerID)
└── ChangeStatus(todoID, customerID, completed)

✅ Mockの定義(Repository)

ここでは「この Repository の関数が呼ばれたら、何を返すか」を定義します。
テスト時に本物の DB を使わず、返り値を自分でコントロールできるのがモックの強みです。

type MockTodoRepository struct {
	mock.Mock
}

func (m *MockTodoRepository) Create(todo *todo.Todo) error {
	args := m.Called(todo)
	return args.Error(0)
}

func (m *MockTodoRepository) GetByID(id int) (*todo.Todo, error) {
	args := m.Called(id)
	return args.Get(0).(*todo.Todo), args.Error(1)
}

func (m *MockTodoRepository) Update(todoID int, todo *todo.Todo) error {
	args := m.Called(todoID, todo)
	return args.Error(0)
}
...

💡 補足

  • m.Called(...)
    →「この関数が呼ばれたよ!」という記録を取ってます
  • Return(...)
    →「呼ばれたときにこれを返す」という挙動を設定できます

✅ テスト例:CreateTodo

  • TODO追加処理の正常系になります。
func TestCreateTodo(t *testing.T) {
	repo := new(MockTodoRepository)
	usecase := NewTodoUsecase(repo)

	repo.On("Create", mock.Anything).Return(nil)

	input := &todo.Todo{
		Title:       "Test Todo",
		Description: "Test Description",
		TeamID:      1,
		CustomerID:  1,
		Completed:   false,
	}

	err := usecase.CreateTodo(input)
	assert.NoError(t, err)

	repo.AssertExpectations(t)
}

💡 補足

  • repo.On("Create", mock.Anything).Return(nil)
    → モックに「Createが呼ばれたら nil を返す」と設定してます。
    mock.Anything は引数の値が何でもOKという指定です。
  • assert.NoError(t, err)
    → usecaseの CreateTodo 実行時にエラーが発生していないかチェックしています。
  • repo.AssertExpectations(t)
    On(...) で定義したメソッドが「本当に呼ばれたかどうか」を検証します(=呼び忘れチェック)。

❌ テスト例:権限チェック(異常系)

  • TODO編集の異常系です(今回はCustomerIDが違う人からリクエストが来た想定)
  • usecaseのUpdateの中でCustomerIDが同じ人かチェックする処理を入れてます。
func TestUpdateUnauthorized(t *testing.T) {
	repo := new(MockTodoRepository)
	usecase := NewTodoUsecase(repo)

	wrongTodo := &todo.Todo{
		CustomerID: 2, // ログインユーザーと違う
		...
	}
	input := &todo.Todo{
		CustomerID: 1,
		...
	}

	repo.On("GetByID", 1).Return(wrongTodo, nil)

	err := usecase.Update(1, input)
	assert.Error(t, err)

	repo.AssertExpectations(t)
}

💡 補足

  • assert.Error(t, err)
    → 権限エラーなどが期待どおり発生しているかをチェックしています。
  • repo.AssertExpectations(t)
    → モックの GetByID が正しく呼ばれているか確認します。

✅ まとめ

  • Goのユニットテストは Testify でだいぶ楽になる
  • Clean Architectureでは、Usecase層に対するテストが一番実装・確認しやすい
  • モックを使うことでDBに依存せず、ビジネスロジックのみを検証できるのが◎

testifyを使ったCRUDユースケースのテスト、慣れると本当に楽です。
同じように悩んでた方の参考になれば嬉しいです!
あと、いろんなエンジニアさんと仲良くなりたいのでXのフォローもぜひ笑

Discussion