コンテンツにスキップ

testify@Goユニットテスト

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. testifyとは

モック、スタブ、アサーションの関数を作成する。

Goではオブジェクトの概念がないため、モックオブジェクトとは言わない。


02. testifyの仕組み

  1. mockパッケージのモック構造体に関数を設定する。
  2. On関数やCalled関数を使用し、モック構造体に仮の関数を定義する。
  3. mockパッケージのモック構造体でAssert*****関数を実行し、期待値と実際値を比較する。
  4. assertパッケージで検証関数を実行し、期待値と実際値を比較する。


03. mock

Mock

▼ Mockとは

構造体のフィールドにモック構造体を設定すれば、その構造体をモック化できる。

*実装例*

AWSクライアントをモック構造体化する。

package amplify

import (
    "github.com/stretchr/testify/mock"
)

/**
 * AWSクライアントをモック構造体化します。
 */
type MockedAwsClient struct {
    mock.Mock
}

▼ On

モック構造体の仮の関数に処理を設定する。

package user

type UserInterface interface {
    Get(id int) (*model.User, error)
}

type User struct {
    UserInterface
}

func (u *User) UserName(id int) (string, error) {

    usr, err := u.Get(id)

    if usr == nil || err != nil {
        return "", err
    }

    return usr.Name, nil
}
package test

import (
    "github.com/stretchr/testify/mock"
)

// MockUserInterface UserInterfaceのモック構造体
type MockUserInterface struct {
    mock.Mock
}

// モック構造体に紐づける仮の関数
func (m *MockUserInterface) Get(id int) (*model.User, error) {

    // On関数で設定された値を受け取る
    arguments := m.Called(id)

    // 本物の関数を同じ返却値にする
    return arguments.Get(0).(*model.User), ret.Error(1)
}

func TestUser_UserName(t *testing.T) {

    testUser := &model.User{ID: 1, Name: "Tom", Gender: model.Male, CreatedAt: time.Now(), UpdatedAt: time.Now()}

    mockUser := new(user.MockUserInterface)

    // Get関数内部のCalled関数に、仮の処理 (ここでは引数と返却値) を設定する
    // 引数と返却値のデータ型は、仮のGet関数と一致させる
    mockUser.On("Get", testUser.ID).Return(testUser, nil)

    u := &User{
        mockUser,
    }

    got, err := u.UserName(testUser.ID)

    if err != nil {
        t.Error(err)
    }
}

▼ Called

モック構造体に仮の関数を紐づける場合に使用する。

On関数で設定された引数や返却値を取得する。

*実装例*

package user

type UserInterface interface {
    Get(id int) (*model.User, error)
}

type User struct {
    UserInterface
}

func (u *User) UserName(id int) (string, error) {

    usr, err := u.Get(id)

    if usr == nil || err != nil {
        return "", err
    }

    return usr.Name, nil
}
package test

import (
    "github.com/stretchr/testify/mock"
)

type MockUserInterface struct {
    mock.Mock
}

// Mock構造体に紐づける仮の関数
func (m *MockUserInterface) Get(id int) (*model.User, error) {

    // On関数で設定された値を受け取る
    arguments := m.Called(id)

    // 本物の関数を同じ返却値にする
    return arguments.Get(0).(*model.User), arguments.Error(1)
}

func TestUser_UserName(t *testing.T) {

    testUser := &model.User{ID: 1, Name: "Tom", Gender: model.Male, CreatedAt: time.Now(), UpdatedAt: time.Now()}

    mockUser := new(user.MockUserInterface)

    // Get関数内部のCalled関数に、仮の処理 (ここでは引数と返却値) を設定する
    // 引数と返却値のデータ型は、仮のGet関数と一致させる
    mockUser.On("Get", testUser.ID).Return(testUser, nil)

    u := &User{
        mockUser,
    }

    got, err := u.UserName(testUser.ID)

    if err != nil {
        t.Error(err)
    }
}

*実装例*

package amplify

import (
    "context"
    "github.com/stretchr/testify/mock"

    aws_amplify "github.com/aws/aws-sdk-go-v2/service/amplify"
)

type MockedAmplifyAPI struct {
    mock.Mock
}

// モック構造体に紐づける仮の関数
func (m *MockedAmplifyAPI) GetBranch(
    ctx context.Context,
    params *aws_amplify.GetBranchInput,
    optFns ...func(*aws_amplify.Options)
    ) (*aws_amplify.GetBranchOutput, error) {

    // On関数で設定された値を受け取る
    arguments := m.Called(ctx, params, optFns)

    // 本物の関数を同じ返却値にする
    return arguments.Get(0).(*aws_amplify.GetBranchOutput), arguments.Error(1)
}

▼ AssertExpectations

モック構造体のOn関数やRetuen関数が正しく実行されたか否かを検証する。

検証対象は、ユーザー定義箇所ではなく、mockパッケージのモック構造体である。

package user

type user interface {
    GetAge() int
}

func isAdult(u user) bool {

    age := u.GetAge()

    return age >= 20
}
package test

import (
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/assert"
)

// MockedUser Userのモック構造体
type MockedUser struct {
    mock.Mock
}

// Mock構造体に紐づける仮の関数
func (m *MockedUser) GetAge() int {

    // On関数で設定された値を受け取る
    args := m.Called()

    return args.Int(0)
}

func Test_Mock(t *testing.T) {

    mockUser := new(MockedUser)

    // GetAge関数内部のCalled関数に、仮の処理 (ここでは返却値) を設定する
    // 引数と返却値のデータ型は、仮のGetAge関数と一致させる
    mockUser.On("GetAge").Return(20)

    // テストを実施する
    result := isAdult(mockUser)

    // 期待値と実際値を比較する
    mockUser.AssertExpectations(t)
    assert.True(t, result)
}

▼ AssertNumberOfCalls

モック構造体内の関数がコールされた回数を検証する。

検証対象は、ユーザー定義箇所ではなく、mockパッケージのモック構造体である。

package user

type user interface {
    GetAge() int
}

func isAdult(u user) bool {

    age := u.GetAge()

    return age >= 20
}
package test

import (
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/assert"
)

// MockedUser Userのモック構造体
type MockedUser struct {
    mock.Mock
}

// Mock構造体に紐づける仮の関数
func (m *MockedUser) GetAge() int {

    // On関数で設定された値を受け取る
    args := m.Called()

    return args.Int(0)
}

func Test_Mock(t *testing.T) {

    mockUser := new(MockedUser)

    // GetAge関数内部のCalled関数に、仮の処理 (ここでは返却値) を設定する
    // 引数と返却値のデータ型は、仮のGetAge関数と一致させる
    mockUser.On("GetAge").Return(20)

    // テストを実施する
    result := isAdult(mockUser)

    // 期待値と実際値を比較する
    mockUser.AssertExpectations(t)
    mockUser.AssertExpectations(t, "GetAge", 1) // GetAge関数をコールした回数を検証する
    assert.True(t, result)
}

https://dev.classmethod.jp/articles/go-testify/#toc-5


Argument

▼ Argumentとは

モック構造体に渡した引数を保持する。

▼ Get

Called関数の設定値をインデックス数で指定し、設定値を取得する。

Called関数に渡される設定値はOn関数が元になっている。

*実装例*

package user

type UserInterface interface {
    Get(id int) (*model.User, error)
}

type User struct {
    UserInterface
}

func (u *User) UserName(id int) (string, error) {

    usr, err := u.Get(id)

    if usr == nil || err != nil {
        return "", err
    }

    return usr.Name, nil
}
package test

import (
    "github.com/stretchr/testify/mock"
)

type MockUserInterface struct {
    mock.Mock
}

// Mock構造体に紐づける仮の関数
func (m *MockUserInterface) Get(id int) (*model.User, error) {

    // On関数で設定された値を受け取る
    arguments := m.Called(id)

    // 本物の関数を同じ返却値にする
    return arguments.Get(0).(*model.User), arguments.Error(1)
}

func TestUser_UserName(t *testing.T) {

    testUser := &model.User{ID: 1, Name: "Tom", Gender: model.Male, CreatedAt: time.Now(), UpdatedAt: time.Now()}

    mockUser := new(user.MockUserInterface)

    // Get関数内部のCalled関数に、仮の処理 (ここでは引数と返却値) を設定する
    // 引数と返却値のデータ型は、仮のGet関数と一致させる
    mockUser.On("Get", testUser.ID).Return(testUser, nil)

    u := &User{
        mockUser,
    }

    got, err := u.UserName(testUser.ID)

    if err != nil {
        t.Error(err)
    }
}

*実装例*

package amplify

import (
    "context"
    "github.com/stretchr/testify/mock"

    aws_amplify "github.com/aws/aws-sdk-go-v2/service/amplify"
)

type MockedAmplifyAPI struct {
    mock.Mock
}

// Mock構造体に紐づける仮の関数
func (m *MockedAmplifyAPI) GetBranch(
    ctx context.Context,
    params *aws_amplify.GetBranchInput,
    optFns ...func(*aws_amplify.Options)
    ) (*aws_amplify.GetBranchOutput, error) {

    // On関数で設定された値を受け取る
    arguments := m.Called(ctx, params, optFns)

    // 本物の関数を同じ返却値にする
    return arguments.Get(0).(*aws_amplify.GetBranchOutput), arguments.Error(1)
}

▼ Error

Called関数の設定値をインデックス数で指定し、設定値を取得する。

Called関数の設定値に誤りがあれば、エラーを返却する。

Called関数に渡される設定値はOn関数が元になっている。

*実装例*

package user

type UserInterface interface {
    Get(id int) (*model.User, error)
}

type User struct {
    UserInterface
}

func (u *User) UserName(id int) (string, error) {

    usr, err := u.Get(id)

    if usr == nil || err != nil {
        return "", err
    }

    return usr.Name, nil
}
package test

import (
    "github.com/stretchr/testify/mock"
)

type MockUserInterface struct {
    mock.Mock
}

// Mock構造体に紐づける仮の関数
func (m *MockUserInterface) Get(id int) (*model.User, error) {

    // On関数で設定された値を受け取る
    arguments := m.Called(id)

    // 本物の関数を同じ返却値にする
    return arguments.Get(0).(*model.User), arguments.Error(1)
}

func TestUser_UserName(t *testing.T) {

    testUser := &model.User{ID: 1, Name: "Tom", Gender: model.Male, CreatedAt: time.Now(), UpdatedAt: time.Now()}

    mockUser := new(user.MockUserInterface)

    // Get関数内部のCalled関数に、仮の処理 (ここでは引数と返却値) を設定する
    // 引数と返却値のデータ型は、仮のGet関数と一致させる
    mockUser.On("Get", testUser.ID).Return(testUser, nil)

    u := &User{
        mockUser,
    }

    got, err := u.UserName(testUser.ID)

    if err != nil {
        t.Error(err)
    }
}

*実装例*

package amplify

import (
    "context"
    "github.com/stretchr/testify/mock"

    aws_amplify "github.com/aws/aws-sdk-go-v2/service/amplify"
)

type MockedAmplifyAPI struct {
    mock.Mock
}

// Mock構造体に紐づける仮の関数
func (m *MockedAmplifyAPI) GetBranch(
    ctx context.Context,
    params *aws_amplify.GetBranchInput,
    optFns ...func(*aws_amplify.Options)
    ) (*aws_amplify.GetBranchOutput, error) {

    // On関数で設定された値を受け取る
    arguments := m.Called(ctx, params, optFns)

    // 本物の関数を同じ返却値にする
    return arguments.Get(0).(*aws_amplify.GetBranchOutput), arguments.Error(1)
}


04. assert

事前処理と事後処理

テスト関数を実行する直前に、事前処理を実行する。

モック構造体の作成のために使用すると良い。

事前処理と事後処理については、以下のリンクを参考にせよ。

よく使用する関数 実行タイミング 説明
SetupSuite 1 テストスイート内の全てのテストの事前処理として、一回だけ実行する。
SetupTest 2 テストスイート内の各テストの事前処理として、テストの度に事前に実行する。BeforeTest関数よりも前に実行されることに注意する。
BeforeTest 3 テストスイート内の各テストの直前の事前処理として、テストの度に事前に実行する。必ず、『suiteName』『testName』を引数として設定する必要がある。
AfterTest 4 テストスイート内の各テストの直後の事後処理として、テストの度に事後に実行する。必ず、『suiteName』『testName』を引数として設定する必要がある。
TearDownTest 5 テストスイート内の各テストの事後処理として、テストの度に事後に実行する。BeforeTest関数よりも後に実行されることに注意する。
TearDownSuite 6 テストスイート内の全てのテストの事後処理として、一回だけ実行する。

*実装例*

事前にモック構造体を作成するために、BeforeTest関数を使用する。

package foo

import (
    "testing"

    "github.com/stretchr/testify/suite"
)

/**
 * ユニットテストのテストスイートを構成する。
 */
type FooSuite struct {
    suite.Suite
    fooMock *FooMock
}

/**
 * ユニットテストの直前の事前処理を実行する。
 */
func (suite *FooSuite) BeforeTest(suiteName string, testName string) {

    // モック構造体を作成する。
    suite.fooMock = &FooMock{}
}

/**
 * ユニットテストのテストスイートを実行する。
 */
func TestFooSuite(t *testing.T) {
    suite.Run(t, &FooSuite{})
}
package foo

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/suite"
)

/**
 * Method関数が成功することを検証する。
 */
func (suite *FooSuite) TestMethod() {

    suite.T().Helper()

    // 事前処理で作成したモック構造体を使用する。
    fooMock := suite.fooMock

    // 以降にテスト処理
}


結果の検証

▼ Exactly

期待値と実際値 (値、データ型) の整合性を検証する。

ポインタのメモリアドレス値は一致していなくても良い。

▼ Equal

期待値と実際値 (値) の整合性を検証する。

ポインタのメモリアドレス値は一致していなくても良い。

▼ Same

期待値と実際値 (メモリアドレス値) の整合性を検証する。

ポインタのメモリアドレス値が同じであれば、結果的に値とデータ型も同じになる。