Goのinterfaceを使ったmockのテストの書き方を学んだ

Goのinterfaceを使ったmockのテストの書き方を学んだ
目次

プログラミング言語 Go を最近少しづつ触るようになってきました。 主に Python で書いていた簡易なスクリプトを置き換える作業なのですが、 開発の過程で、Go におけるテストコードの書き方を学習したので、備忘としてまとめます。

Goではスタブがいい感じに作れない

JavaやPythonでは、ライブラリの力を借りることによって、スタブを簡単に作ることができました。 特にJavaのどうしようもないレガシーコードと対峙する際には、 jmockit を友として、関数の振る舞いをテストコードで固めた後にリファクタリングを行う、といったファンキーな事をやっていました。

しかし、Goではそのような「リフレクションを使えば何でもあり」なことができません。(もしかしたらできるかもしれませんが、今の所見つけていません)

基本的にはDependency Injectionの発想と同様のことを行います。Interfaceに対して実装を後から定義するのです。 さっそくやってみましょう。

プロダクションコード側

構造体とインタフェースの定義

まずは、構造体 Sample を定義します。構造体には Client interface をもたせます。 外部パッケージからは直接 client を操作できないようにする目的で、doGet 関数でラップします。

 1// 構造体を定義する
 2type Sample struct {
 3  // interfaceを定義する
 4	client Client
 5}
 6
 7// 振る舞いをモックしたい
 8type Client interface {
 9	Get() int
10}
11
12// interfaceのGet()をラップする
13func (sample *Sample) doGet() int {
14	return sample.client.Get()
15}

interfaceの実装

次にinterfaceの実装を行います。 Go のinterfaceは他の言語と異なり、実装する側(ここでいう構造体)による 「このinterfaceを実装しますよ」 という宣言が不要です 。 レシーバを使い、interfaceに規定された関数を持った構造体を定義してあげれば良いのです。 今回は hogeClientImpl という名前で実装します。

1// Client interface の実装をする構造体
2type hogeClientImpl struct {
3}
4
5// レシーバで実装する
6func (client *hogeClientImpl) Get() int {
7	return 1
8}

これによって、プロダクションコードの実装においては、例えば以下のように、hogeClientImpl を外から渡すことができます。 仮に hogeClientImpl の実装がinterfaceの定義を満たしていなければ、コンパイルエラーになります。

1func hogeMain() int {
2  sample := &Sample{&hogeClientImpl{}}
3  // 1が返却される
4	return sample.doGet()
5}

テストコード側

interfaceの実装

次にテストコードを作ってみましょう。テストコードファイル側でも同様に interfaceの定義を満たす構造体を定義してあげればOKです。 ここでは testClientImpl とします。

1type testClientImpl struct {
2}
3
4func (c *testClientImpl) Get() int {
5	return 2
6}

interfaceと同じ関数を規定したので、Client として引数に渡すことが可能となりました。

1func TestMainRequest(t *testing.T) {
2  // testClientImpl の実体をセットする
3  sample := &Sample{&testClientImpl{}}
4  // testClientImpl#Getが呼ばれ、2が返却される
5	res := sample.doGet()
6	if res != 2 {
7		t.Error("response should be 2")
8	}
9}

まとめ

今回はinterfaceを使って Go のテストコードを書いてみました。

  • interfaceを持った構造体を定義する(構造体A)
  • interfaceを実装した構造体を定義する(構造体B)
  • 構造体Aの初期化時に構造体Bを渡してあげる
  • テスト時には、テスト用に作成した構造体(構造体C)を構造体Aの初期化時にわたしてあげる

といった感じです。

interface定義からmockを自動生成してくれるライブラリ もありましたが、当面は自前で構造体定義して頑張ろうと思っています。