この記事は株式会社ネクスト(Lifull) Advent Calendar 2016の2日目の記事です。
みなさんはじめまして、ネクストのchissoです。
はじめてQiitaに投稿するのでドキドキしています。
そんなことはさておき、この記事では、Golangで自動テストしやすいコードを書く方法について、私なりに気づいたことをまとめます。
想定読者
- 普段よくRSpecを書いている
- Golang初心者
- Interfaceってなに?
目指すところ
「メソッド単位でテストコードを実行できる状態にする」
これだけです。
私は普段Rubyを書くことが多く、RSpecを使ってメソッド単位でユニットテストを作成しています。
ですが、Rubyと同じ感覚でコードを書いて、RSpecと同じノリでGolangのテストを書こうとしてハマりました。
(静的型付けの言語・Interfaceに疎く、そもそもコードがダメすぎたことが原因でした。)
GolangではRSpecのようにmock(stub)が作れない
言語仕様として当たり前なのですが、私がなにもわかっていなかった時代に書いていたGolangのコードを、Rubyのコードと比較しつつ進めていきます。
package main
import(
"fmt"
)
type Hoge struct {
hoge string
}
func main() {
h := Hoge{"hogehoge"}
echo(h)
}
func echo(h Hoge) string {
str := h.getHoge()
fmt.Println(str)
return str
}
func (h Hoge) getHoge() string {
return h.hoge
}
class Main
def initialize(hoge)
@hoge_instance = hoge
end
def echo
p @hoge_instance.get_hoge
end
end
class Hoge
def initialize(str)
@hoge = str
end
def get_hoge
@hoge
end
end
package/classの設計とか、Rubyでget_hogeってなんだよ、みたいなツッコミは一旦置いておいて、、
この両者において、echoのテストを書くことを考えてみます。
先にRSpecを使う前提でRubyから。
require './sample.rb'
describe Main do
# とりあえずechoのテストだけ
describe '#echo' do
let(:hoge) { double('hoge') }
let(:str) { 'hogehoge' }
let(:target) { described_class.new(hoge) }
subject { target.echo }
before do
# Hogeのメソッドはモックする
allow(hoge).to receive(:get_hoge).and_return(str)
end
it 'get_hogeの値をpすること' do
expect(target).to receive(:p).with(str)
subject
end
end
end
ではGolangはというと・・・さきほどのsample1.goをベースにすると、echoのユニットテストは書けません。
Hogeというstruct、そしてgetHogeというメソッドを上書きすることができないため、mockすることができないのです。
今回は簡単のため同一ファイル(package)内でstructを作成していますが、私は当初packageをまたいでこのようなコードを書いてしまっていました。
ユニットテストが書きたいのに、他のpackageの実装に依存したテストコードになってしまうのです。
日々悩み、ネットの海を徘徊し、辿り着いた答えが下記となります。
##解決策: interfaceを使う
Golangを使い始めた最初は、なんでも突っ込める魔法のビンのように思っていたinterface{}。
interfaceの意味を理解して使いこなすことで、ユニットテスト可能なpackage(≒ class)を作れるようになれます。
Golangのinterfaceについては、下記の記事が詳しいです。
interfaceを使ってsample1.goを書き直してみます。
package main
import(
"fmt"
)
// Hoge structをラップするかたちでinterfaceを宣言
type HogeInterface interface {
getHoge() string
}
type Hoge struct {
hoge string
}
func main () {
h := Hoge{"hogehoge"}
echo(h)
}
// 引数をstructではなくinterfaceにする
func echo(h HogeInterface) string {
str := h.getHoge()
fmt.Println(str)
return str
}
func (h Hoge) getHoge() string {
return h.hoge
}
たったこれだけの変化で、echoがユニットテスト可能になります。
package main
import (
"testing"
)
// Hogeのモックを宣言
// specの let(:hoge) { double('hogehoge') }に相当
type HogeMock struct {}
// HogeMockにgetHogeを実装
// specの allow(hoge).to receive(:get_hoge).and_return("hogehoge")に相当
func (h HogeMock) getHoge() string {
return "hogehoge"
}
func TestEcho(t *testing.T) {
mock := HogeMock{}
hoge := echo(mock)
if hoge != mock.getHoge() {
t.Error("error!")
}
}
このようにstructをinterfaceでラップすることで、他のメソッドやpackageとの依存関係はその”振る舞い”(= interface)だけに留めることができ、各メソッドでユニットテストを作成することができるようになります。
これをやった上で、凝ったテストをやりたい場合にはテスト用のライブラリを使うとはかどります。
私はやはりRSpec風に書きたかったので、ginkgoというライブラリを使いました。
ただし、これだけでは対応できないパターンもあったので、テストしやすいコードを書く方法をもう一つご紹介しておきます。
##関数を変数化する
私は下記の記事でこの方法を学びました。
Golangで関数をグローバル変数に代入してテスト時にスタブする
実際のコードに近い形で例を示します。
###API接続packageとmodel packageを切り離す
APIからデータを取得して整形する、modelに相当するようなpackageがあるとして、このmodelをテストする時は実際にAPIを叩きたくありません。
そこで、APIのclientを担うpackageを作成し、そのclientを使ってAPIを呼び出すメソッドを変数化しておきます。
package client
import(
"encoding/json"
"io/ioutil"
"net/http"
)
type clientInterface interface {
buildUri(string, string) string
buildQueryString(map[string]string) string
getHttpClient() http.Client
}
type client struct {
host string
port int
timeout int
}
var c clientInterface
func init() {
c = &client{
host: "www.example.com",
port: 80,
timeout: 5,
}
}
// APIからjsonでデータを受け取り、任意のstructにparseする
var Get = func (path string, params map[string]string, target interface{}) error {
p := c.buildQueryString(params)
uri := c.buildUri(path, p)
httpClient := c.getHttpClient()
response, err := httpClient.Get(uri)
if err != nil {
return err
}
defer response.Body.Close()
body, err := ioutil.ReadAll(reponse.Body)
if err != nil {
return err
}
err = json.Unmarshall(body, &target)
if err != nil {
return err
}
return nil
}
// 以下、clientInterfaceはいい感じに実装します。
API接続packageをこのようにしておくことで、取得パスに応じたmodelの作成を行うことができます。(さらにPost/Delete/Putをapi.go内で作成する場合も同様)
そして、Getを下記のようにモックしてやれば、それぞれのmodelでテストが行えます。
// modelのpackageで、上記client.goをclientとしてimportする想定。
// 変数として宣言したGetメソッドを上書きする
client.Get = func(path string, params map[string]string, target interface{}) error {
j := `{"id": 1, "name": "chisso"}`
json.Unmarshall(j, &target)
return nil
}
// error時をテストしたいときも自在
client.Get = func(path string, params map[string]string, target interface{}) error {
return errors.New("Not Found")
}
エラー時の再現が自在になる恩恵は大きいと思います。
clientで起こった各種errorをmodelできちんとハンドリングする場合に、モックしたGetメソッドで各種エラーを引き起こせるので、とてもテストしやすいです。
なお、client自身もinterface化しているので、Get自身もきちんとユニットテスト可能です。
また、複数のAPIを利用するような場合であれば、コンストラクタを変数として提供することでそれぞれうまくモックすることができます。
##まとめ
いかがでしたでしょうか。
interfaceをきちんと理解している人にとってはとても当たり前の話だったかもしれません。
ただ、もし自分のようにphpやRubyからプログラミングに入り、テストコード書いてたけどinterfaceの概念がうまく理解できてなかった人の助けになれば幸いです。
株式会社ネクスト(Lifull) Advent Calendar 2016の3日目の記事は私の部署の先輩でもあるzomさんです。
ご期待下さい!