はじめに
今回はモックオブジェクトについての説明と、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ライフを!