GoMockで、構造体オブジェクトのポインタを受け取る関数の引数をテストする


概要

モック化させたインターフェースの関数に、期待した値が入って呼ばれるかどうかをテストしたいシーンのお話です。

引数に構造体のポインタを受け取る関数をテストする時、

  • テストスイート内でアサーションのために宣言したオブジェクト
  • 実際のビジネスロジックの中で生成されたオブジェクト

はポインタが異なるため、オブジェクトのチェックが難しくなります。

gomockを使っている際に、この問題を解決する方法をまとめます。

GoMock

GitHub: https://github.com/golang/mock

今回使用したバージョンはv1.4.3です。

ユースケース

概要だけだとイメージが湧きにくいかと思いますので、ここからは例を交えて説明します。
ユースケースは下記のようにシンプルなものを用意します。

  • タイトルと著者名を入力して、Bookオブジェクトを作成する
  • タイトルに空文字を入れた時には、Bookオブジェクトのタイトルが「no title」になる

テストケース

テストでは、

  • When: ユースケースの関数に空文字のタイトルを渡した時
  • Then: DBに保存されるエンティティのタイトルに「no title」がセットされている

ということをチェックします。

ビジネスロジック実装

Entity

ID、 タイトル、 著者名を持ちます。

book.go
package entity

type Book struct {
    ID     uint64
    Title  string
    Author string
}

func NewBook(title, author string) Book {
    return Book{
        Title:  title,
        Author: author,
    }
}

Repository

entityを永続化させるインターフェースです。

book_repository.go
package repository

import "github.com/.../entity"

type BookRepository interface {
    // Bookオブジェクトのポインタを受け取り、DBに保存する
    Save(book *entity.Book) error
}

RepositoryのSave関数がポインタを受け取っています。
このインターフェースを、GoMockでモック化させます。

Interactor

そしてユースケースに該当する、Interactorの関数を今回はテストします。

book_interactor.go
package service

import (
    "fmt"

    "github.com/.../entity"
    "github.com/.../repository"
)

type BookInteractor struct {
    bookRepository repository.BookRepository
}

func (bi BookInteractor) CreateBook(title, author string) error {
    // titleが空の場合はno titleを入れる
    if title == "" {
        title = "no title"
    }

    // entity作成
    book := entity.NewBook(title, author)

    // entityをDBに保存
    err := bi.bookRepository.Save(&book)
    if err != nil {
        return fmt.Errorf("failed to save book: %w", err)
    }

    return nil
}

テストコード

失敗するケース

book_interactor_test.go
package service

import (
    "testing"

    "github.com/golang/mock/gomock"
    "github.com/.../entity"
    mock_repository "github.com/.../mock/repository"
)

func TestBookInteractor_CreateBook(t *testing.T) {
    controller := gomock.NewController(t)
    defer controller.Finish()

    t.Run("succeed when name is empty", func(t *testing.T) {
        // テストする関数の引数に入れる値
        argTitle := "" // When: タイトルに空を指定する
        argAuthor := "taro"

        // bookRepositoryをモック化
        bookRepository := mock_repository.NewMockBookRepository(controller)

        // bookRepository.Save関数の引数に期待するオブジェクト
        expectedBook := entity.Book{
            ID:     0,
            Title:  "no title", // Then: no titleがセットされている
            Author: "taro",
        }

        // bookRepository.Save関数に期待した引数が入って呼ばれるかチェック
        bookRepository.EXPECT().Save(&expectedBook).Return(nil)

        interactor := BookInteractor{
            bookRepository: bookRepository,
        }

        // action: テスト対象関数呼び出し
        err := interactor.CreateBook(argTitle, argAuthor)
        if err != nil {
            t.Fatal(err)
        }
    })
}

Save関数が呼ばれる時の引数の構造体の中身は、想定する expectedBook と同じはずですが、このケースでは失敗します。

なぜなら、Save関数が呼ばれる時には引数がポインタで渡されています。
そして、この expectedBook と、interactorの中で NewBook で生成されたオブジェクトの中身は同じですが、オブジェクト自体のポインタが異なるためにエラーが吐かれます。

この場合にどうすれば良いかというと、2つやり方があります。

  • 構造体の中身をアサーションするカスタムMatcherを作る
  • mockに用意された Do メソッドを使って、構造体の中身を1つずつアサーションする

前者は少々面倒なので、後者で回避します。

成功するケース

book_interactor_test.go
package interactor

import (
    "testing"

    "github.com/golang/mock/gomock"
    "github.com/.../entity"
    mock_repository "github.com/.../mock/repository"
    "github.com/stretchr/testify/assert"
)

func TestBookInteractor_CreateBook(t *testing.T) {
    controller := gomock.NewController(t)
    defer controller.Finish()

    t.Run("succeed when name is empty", func(t *testing.T) {
        // テストする関数の引数に入れる値
        argTitle := "" // When: タイトルに空を指定する
        argAuthor := "taro"

        // bookRepositoryをモック化
        bookRepository := mock_repository.NewMockBookRepository(controller)

        // bookRepository.Save関数に期待した引数が入って呼ばれるかチェック
        bookRepository.EXPECT().Save(gomock.Any()).Return(nil).Do(func(actualBook *entity.Book) {
            // 変更箇所
            assert.Equal(t, "no title", actualBook.Title) // Then: no titleがセットされている
            assert.Equal(t, "taro", actualBook.Author)
        })

        interactor := BookInteractor{
            bookRepository: bookRepository,
        }

        // action: テスト対象関数呼び出し
        err := interactor.CreateBook(argTitle, argAuthor)
        if err != nil {
            t.Fatal(err)
        }
    })
}

GoMockには、モックオブジェクトの関数の処理後にフックされる Doメソッドがあり、それを使用します。
Doに渡すコールバック関数の中に、Saveメソッドが呼ばれた時の実際の引数が入ってきます。
それを対象にコールバック関数内でアサーションを行います。

ポインタで引数が渡されるなら、渡ってきたオブジェクトの中身を1つずつアサーションしてしまおう という戦法です。

なお、Save関数の引数に、gomock.Any() という「どんな引数でも期待する」というMatcherを入れないと、モック関数の呼び出しでコケます。

最後に

GoMockで、構造体オブジェクトのポインタを受け取る関数の引数をテストする方法をまとめました。
もしもっとスマートにテストできる方法があれば教えていただけますと嬉しいです!