Mock ライブラリを使わない独自のMock でテスト自動化をする
Go 言語ではMock ライブラリとして幾つか有名なものがありますが、約1.5ヶ月それらを使ってGo 言語でMock を使ったテストをトライアンドエラーで勉強していましたが現段階で独自にMock を使ってテストする方法に落ち着きました。
ここではそれらの勉強でMock ライブラリを使わずに独自のMock を作成してテストを自動化する方法について記録として残しておこうと思います。
説明は完成前のプログラムと完成後のプログラムをまず提示し、個々に説明していく形式で実施していきます。
サンプルプログラム
テスト自動化のサンプルとして"http://httpbin.org/ip" にGET リクエストを送信し、レスポンスJSON を解析して自分のグローバルIP アドレスを取得するプログラムを作成しました。
package main
import (
"net/http"
"io/ioutil"
"encoding/json"
"errors"
"fmt"
)
const url = "http://httpbin.org/ip"
func GetGlobalIP() (string, error) {
response, err := http.Get(url)
if err != nil {
fmt.Println("ERROR: Requesting " + url + " has failed")
return "", err
}
defer response.Body.Close()
byteJSON, _ := ioutil.ReadAll(response.Body)
if response.Status != "200 OK" {
fmt.Println("ERROR: Status of response from " + url + " was " + response.Status)
return "", errors.New("ERROR: Got response status " + response.Status + " from " + url)
}
ipv4, _ := fetchIPFromJSON(byteJSON)
return ipv4, nil
}
func fetchIPFromJSON(jsonByte []byte) (string, error) {
var unmarshaled interface{}
err := json.Unmarshal(jsonByte, &unmarshaled)
if err != nil {
return "", err
}
jsonMap := unmarshaled.(map[string]interface{})
return jsonMap["origin"].(string), nil
}
func main() {
ipv4, _ := GetGlobalIP()
fmt.Println("Global IP is " + ipv4)
}
/* 以下略... */
$ go run globalip.go
Global IP is 9.9.9.9
上記のプログラムをテストする場合http.Get
, ioutil.ReadAll
をMock 化してテスト自動化を行いたいところですがGo 言語の仕様上、このままのコードでは難しそうです。
そのため上記のプログラムをテスタブルに書き換えてMock を使ったテスト自動化をできるようにしましょう。
テスタブルに書き換える
テスタブルに書き換えた結果、プログラム本体は次のようになります。
package main
import (
"net/http"
"io/ioutil"
"encoding/json"
"errors"
"fmt"
"io"
)
const url = "http://httpbin.org/ip"
type GlobalIPUtil struct {
httpGet func(string) (*http.Response, error)
ioutilReadAll func(io.Reader) ([]uint8, error)
}
func NewGlobalIPUtil() (*GlobalIPUtil) {
globalIPUtil := &GlobalIPUtil {
httpGet: http.Get,
ioutilReadAll: ioutil.ReadAll,
}
return globalIPUtil
}
func (self *GlobalIPUtil) GetGlobalIP() (string, error) {
response, err := self.httpGet(url)
if err != nil {
fmt.Println("ERROR: Requesting " + url + " has failed")
return "", err
}
defer response.Body.Close()
byteJSON, _ := self.ioutilReadAll(response.Body)
if response.Status != "200 OK" {
fmt.Println("ERROR: Status of response from " + url + " was " + response.Status)
return "", errors.New("ERROR: Got response status " + response.Status + " from " + url)
}
gip, _ := self.fetchIPFromJSON(byteJSON)
return gip, nil
}
func (self *GlobalIPUtil) fetchIPFromJSON(jsonByte []byte) (string, error) {
var unmarshaled interface{}
err := json.Unmarshal(jsonByte, &unmarshaled)
if err != nil {
return "", err
}
jsonMap := unmarshaled.(map[string]interface{})
return jsonMap["origin"].(string), nil
}
func main() {
iputil := NewGlobalIPUtil()
ipv4, _ := iputil.GetGlobalIP()
fmt.Println("Global IP is " + ipv4)
}
/* 以下略... */
それに対し、テストコードとしては以下のようになります。
package main
import (
"testing"
"bytes"
"net/http"
"io/ioutil"
"github.com/stretchr/testify/assert"
)
func TestGetGlobalIP(t *testing.T) {
assert := assert.New(t)
// Create mock
iputil := &GlobalIPUtil{
httpGet: func(url string) (*http.Response, error) {
result := &http.Response{
Status: "200 OK",
Body: ioutil.NopCloser(bytes.NewBufferString("{\"origin\":\"1.1.1.1\"}")),
}
return result, nil
},
ioutilReadAll = ioutil.ReadAll,
// 以下のようにMock を定義することも可能
/*
ioutilReadAll = func(io.Reader) ([]uint8, error) {
return []byte("{\"origin\":\"1.1.1.1\"}"), nil
}
*/
}
result, err := iputil.GetGlobalIP()
assert.Nil(err)
assert.Equal("1.1.1.1", result)
}
// .......
ここでテスタブルにするためのポイントを個々に解説していきたいと思います。
ポイントとしては以下のようになります。
- 外部からもアクセスできるstruct (struct 名の頭文字が大文字なやつ)を作成し、その中にMock 化したい関数へのポインタを定義する
- struct のインスタンスを返すNew 関数(NewGlobalIPUtil)を作成する
- 本番時はNewGlobalIPUtil 関数を使って、テスト時は自前でstruct をインスタンス化する
外部からもアクセスできるstruct を作成し、その中にMock 化したい関数へのポインタを定義する
http.Get
関数、ioutil.ReadAll
関数に対するポインタを格納するstruct を定義します。
......
type GlobalIPUtil struct {
httpGet func(string) (*http.Response, error) /* 関数ポインタ */
ioutilReadAll func(io.Reader) ([]uint8, error) /* 関数ポインタ */
}
......
Go 言語は静的型付けなので関数のポインタを作成するときは正確に引数の型や返り値の型を宣言して上げる必要があります。
それらを調べるにはドキュメントを調べるでもいいですが、ドキュメントが少なかったりソースコードが見当たらない場合は以下のコードを書いて実行してみるでもOK です。
package main
import (
"fmt"
"net/http"
)
func main() {
fmt.Printf("%T\n", http.Get)
}
----
$ go run test.go
func(string) (*http.Response, error)
struct のインスタンスを返すNew 関数を作成する
struct のインスタンスを返すNew 関数を作成しhttp.Get, ioutil.ReadAll へのポインタを持ったstruct を返す関数を作成します。
func NewGlobalIPUtil() (*GlobalIPUtil) {
globalIPUtil := &GlobalIPUtil {
httpGet: http.Get, /* デフォルトでhttp.Get を入れる */
ioutilReadAll: ioutil.ReadAll, /* デフォルトでioutil.ReadAll を入れる */
}
return globalIPUtil
}
本番時はNewGlobalIPUtil 関数を使って、テスト時は自前でstruct をインスタンス化する
ソースコードをテスタブルに書き換えたら、本番ではNewGlobalIPUtil 関数を使って、テストでは自前でstruct をインスタンス化してプログラムを実行します。
......
iputil := NewGlobalIPUtil()
ipv4, _ := iputil.GetGlobalIP()
......
......
iputil := &GlobalIPUtil{
httpGet: func(url string) (*http.Response, error) {
/* Mock プログラムをここに書く */
},
ioutilReadAll: func(io.Reader) ([]uint8, error) {
/* Mock プログラムをここに書く */
},
}
ipv4, _ := iputil.GetGlobalIP()
......
具体例は本記事の最初に示したgithub のURL にあるので参考にしてみてください。
最後に
上記のように作成することで本番用のソースコードにテストのためだけのボイラープレートを極力書かなくて済み、可読性の高さを維持したままMock も利用できるテスタブルなソースコードを利用することができるようになります。
Go 言語でテストの自動化をする時にMock を利用せずに実際にテストサーバを構築してテスト環境の準備にコストがかかっていたり本番用のソースコードにMock を入れた為にしっくり来なかったり…といった悩みから解消されましょう!
参考
-
Golangでテストしやすいコードをかく
-
Proposal Add inject function/struct ability to go test for mock
-
golang/mock, Github
-
matryer/moq, Github
-
stretchr/testify, GitHub
-
vektra/mockery, GitHub
-
DATA-DOG/go-sqlmock, GitHub