[Golang]テストで特定の処理をモックにしたい場合のインターフェースの使い方
Contents
はじめに
最近は会社でGolangの利用が少しずつ広がっています。
そんな中、一人の同僚から、「Golangでプロダクションコードでは期待通りの動きをさせて、テストだとモックにする場合のGolangのやり方を教えて欲しい。」という要望がありました。
せっかくなので、ブログにまとめてみます。
対象はオブジェクト思考な言語の経験はあるけどGolangの経験は浅いような方です。
結論
簡単に書くと以下のことをやればプロダクションコードとテストコードで動作を分けることができます。
- 動作を分けたい対象をinterfaceを使って抽象化する
- プロダクションコードでは対象のinterfaceにプロダクション用の実装をDIする
- テストコードでは対象のinterfaceにテスト用の実装をDIする
ということです。
知っている人は「そんなの当たり前じゃん!」と思うかもしれませんが、この記事ではこれをGolangでやるにはどうしたらよいのか、具体的なコードを用いて紹介していこうと思います。
DIとDIコンテナ
まずはDIとDIコンテナについて整理します。
Java界隈の人がDIという単語を見ると、DIコンテナのことを思い浮かべてしまう人が多いかもしれません。(かく言う自分もDI = DIコンテナと思っていた時期はありました…)
例えば、JavaのSpringを使うとDIしたい対象に特定のアノテーションをつけると、DIコンテナがよきに計らって特定のタイミングでDIしてくれます。
そのため、アプリケーションのコードでは対象のオブジェクトの初期化などを考えずに利用することが可能となります。
SpringのDIサンプル。 @Autowired
というのがDIコンテナにDIしてね、という目印になります。
|
|
このようにDIコンテナを使っていると、上記の userRepository
はアプリケーションコード中ではどこでも初期化していないのに利用することが可能になります。
これを見るとDI(コンテナ)ってブラックボックスでよくわらかない…というイメージを持ってしまうのもしょうがないのかなと思ったりします。
ちょっと前置きが長くなりましたが、DIとDIコンテナは以下のように分類できると理解しています。(もし間違ってたらご指摘ください)
- DI ・・・ 特定の変数に対して何かしらの具体的なオブジェクトを初期化された状態で渡すこと
- DIコンテナ ・・・ DIを自前(アプリケーションコード中で)行うことなく、DIコンテナから欲しいオブジェクトが初期化された状態で受け取ることを出来るようにする仕組み
つまり、DIコンテナがなくてもDIをすることは出来ます。(面倒になるけど)
Golangでのinterfaceの使い方
次に簡単にinterfaceについて紹介しておこうかと思います。
知ってるよ!と言う人は読み飛ばしちゃってください。
まずは超基礎、interfaceの定義の仕方です。
このようにメソッドと引数と返却値の形を決めるものです。
|
|
次にinterfaceについて具体的な実装をしてみます。
|
|
このようにGolangではどのinterfaceの実装をするか、明示的に宣言することなくそのinterfaceを満たしていれば実装されているとみなされます。
これらのチェックはコンパイル時に行われるため実行は高速になります。
さて、実際にこのinterfaceの実装を使ってみます。
|
|
実行してみます。
|
|
どちらも MyInterface
型ですが、結果はそれぞれ具体的な実装である A と B の結果が出力されましたね。
interfaceな変数にDIする
次に上記で作ったinterfaceに対してDIをしてみようと思います。
今回は簡単なサンプルにするため、関数の引数に対してDIしてみます。
まずはprintする簡単な関数を定義します。
|
|
ポイントは引数を具体的なstructではなくinterfaceで受け取っているところです。
このinterfaceに対してDIしてみます。
|
|
最初の PrintAny
は A を DI しています。2番目は B を DI しています。
「え、これだけ?」と思うかもしれませんが、これも立派なDIになります。
このように実装すると何が嬉しいのかというと、 PrintAny
関数の MyInterface
が関数内部でインスタンスを生成していないことで実装を入れ替えることができることです。
ただ、今回のサンプルでは実装を入れ替えることにあまりメリットを感じれないかと思います。
次にDIのメリットがあるようなサンプルを記載します。
プロダクションコードとテストコードで実装を切り替える
ここでは以下のようなサンプルを考えます。
- 現在時刻を取得
- Unix時間に変換
- Unix時間が偶数なら
Even
を、奇数ならOdd
を返す関数を定義する
この問題を単純に解くのであれば以下のようなコードになります。
|
|
簡単ですね。次にこの UnixTimeSample
の関数をテストしてみましょう。
|
|
しかし、このテストは green
になったり red
になったりします。
|
|
まあ当たり前ですよね、現在時刻に依存しているので。
このように外部の要素に依存するものを関数内部で生成してしまうとユニットテストがうまくできなくなってしまいます。
そこで、DIパターンを使って外から外部依存する対象を注入してしまえばユニットテストでロジックのテストを行うことができるようになります。
リファクタリングしてみましょう。今回は対象が時間を扱っています。そこで、 TimeManager
を導入してみます。
|
|
現在時刻を取得する ITimeManager
というインターフェースを定義しました。
次にプロダクションコードを実装してみます。
|
|
プロダクションコードでは標準の現在時刻を取得する関数を呼び出しているだけです。
UnixTimeSample
で使っている現在時刻取得を TimeManager
経由で行うようにしてみます。
|
|
メソッド引数で受け取るようにしました。
そして、最後にメイン関数で TimeManager
をDIします。
|
|
試しに実行してみましょう。時間に依存するのでタイミングによってそれぞれ表示が変わりますね。
|
|
これでプロダクションコードは挙動を変えることなく TimeManager
の導入ができました。
次にテストコードでロジックのテストをしてみます。
まずは TimeManager
のモックを導入します。
|
|
この MockTimeManager
では外から好きな時間をNowとして取得可能なようにしています。
これを使ってテストを実行してみます。テストはテーブルドリブンテストの手法で作ってみます。
|
|
これを実行してみると、見事にfailしました。
|
|
まとめ
今回は対象としてわかりやすいと思う時間についてGolangのinterfaceを使ってプロダクションコードとテストコードで実装を入れ替えるDIのやり方を紹介しました。
DIについて理解できたでしょうか?
実際にやってみると意外と簡単だった思うのではないでしょうか。
今回のサンプルは以下のリポジトリにコードを置いています。
https://github.com/ken-aio/go-interface-sample
もしDIの対象が増えて自前で管理するのが辛くなってきた時はDIコンテナの出番です。
GolangではDI管理についてはGoogleが作っている wire
というライブラリを使っている例をよく見かけます。(自分では使ったことはありません…)
https://github.com/google/wire
比較的大きなプロジェクトで色々な場面でDIを使う必要が出てきた場合は導入の検討をしてみると良いかもしれません。
本記事が誰かしらの役に立てば幸いです。
ではまたいつか
Author ken-aio
LastMod 2019-10-17