11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Goのモックオブジェクトと自動生成

Last updated at Posted at 2020-12-04

はじめに

今回はモックオブジェクトについての説明と、Goではどのように実装できるかご紹介したいと思います。
その実装をサポートするためのsimplemockというモック自動生成ツールを開発したので、ついでにその紹介もさせていただきます。

モックオブジェクトとは?

ユニットテストを実行時、制御できないような外部コンポーネントに依存している場合、その箇所を代用品に置き換えてテストをすることが多いと思います。
そのようなテストにおける代替品のことを総じてテストダブル(Test Double)といいます。
例えば、クラウドのエミュレータもフェイクオブジェクトと呼ばれるテストダブルのうちの一つです。
テストダブルについては[xUnit](http://xunitpatterns.com/Test Double.html)に詳しくまとまっているので、気になる方は参照してください。

複数あるテストダブルの中でモックオブジェクトとは、テスト対象の間接出力を検証できるオブジェクトのことを指します。このモックオブジェクトは通常、テストダブルの中の一つであるテストスタブの機能も含まれています。テストスタブとはテスト対象に間接入力を行うものです。

Goにおけるモックオブジェクト

例えば以下のような簡単な実装があるとしてService#GetUserを検証対象としましょう。
Service#GetUserはユーザIDを入力として、他のサービスに問い合わせをしてUserを返す仕様です。
他サービスへの問い合わせはHTTPClientというインターフェースを使っていて中の実装を隠蔽しています。

今回はServiceが依存しているHTTPClientをモックオブジェクトに置き換えて検証できるようにしてみましょう。

import (
	"context"
	"encoding/json"
	"net/http"
	"net/url"
	"strconv"
)

type HTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

type Service struct {
	HTTPClient HTTPClient
}

func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
	u, err := url.Parse("http://userservice/")
	if err != nil {
		return nil, err
	}
	q := u.Query()
	q.Set("id", strconv.Itoa(id))
	u.RawQuery = q.Encode()
	req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := s.HTTPClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	respBody := struct {
		User *User `json:"user"`
	}{}
	json.NewDecoder(resp.Body).Decode(&respBody)

	return respBody.User, nil
}

まず、モックオブジェクトを利用する際、検証ロジックをモックオブジェクトに対して実装する必要があります。

例えば、以下のようにHTTPClientインターフェースを満たすようなモックオブジェクトの構造体を定義すると、フィールドの関数を差し替えることによってテスト毎に検証ロジックを注入することが可能です。

type httpClientMock struct {
	DoFunc func(req *http.Request) (*http.Response, error)
}

func (m *httpClientMock) Do(req *http.Request) (*http.Response, error) {
	if m.DoFunc != nil {
		return m.DoFunc(req)
	}
	return nil, errors.New("implement Do")
}

以下のテストコードでは先ほど定義したモックオブジェクトの構造体を利用して、引数の*http.Requestを検証しています。

func TestService_GetUser_Request(t *testing.T) {
	id := 10000
	client := &httpClientMock{
		DoFunc: func(req *http.Request) (*http.Response, error) {
			if got, want := req.URL.String(), "http://userservice/?id=10000"; got != want {
				t.Errorf("url: %s, want: %s", got, want)
			}
			return httptest.NewRecorder().Result(), nil
		},
	}
	s := Service{client}
	s.GetUser(context.Background(), id)
}

モックオブジェクトで戻り値を定義することでスタブとして利用することもできます。
以下のコードではnet/http/httptest.ResponseRecorderを使ってHTTPレスポンスを生成しています。

func TestService_GetUser_Response(t *testing.T) {
	client := &httpClientMock{
		DoFunc: func(req *http.Request) (*http.Response, error) {
			w := httptest.NewRecorder()
			respBody := struct {
				User *User `json:"user"`
			}{
				User: &User{
					ID:   1,
					Name: "testuser",
				},
			}
			json.NewEncoder(w).Encode(&respBody)
			return w.Result(), nil
		},
	}
	s := Service{client}
	got, err := s.GetUser(context.Background(), 1)
	if err != nil {
		t.Fatal(err)
	}
	want := &User{ID:1, Name: "testuser"}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("GetUser() got = %v, want %v", got, want)
	}
}

モックオブジェクトのコード自動生成

github.com/golang/mockというツールはあるですが、メソッドチェーンを使ってDSL形式のようにスタブの定義をできるようにしている為、実装がやや複雑で利用するにもハードルが少し高い気がします。

今回のように、シンプルな実装を生成するジェネレーターが欲しかったのでsimplemockというツールを開発しました。
インターフェースをインプットとしてコードを自動生成するツールです。もしよかったらご利用ください!

最後に

このように外部依存をインターフェースを使って実装を隠蔽することによって、中の実装を差し替えることでテスト容易性の高いアプリケーションを開発することができます。
今回はインターフェースを使って差し替えられるようなご紹介をしましたが、状態を持たず外から実装を差し込めるようなクロージャーを使っても良いと思います。

それでは、よいGoライフを!

11
8
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
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?