最近golangを結構書いています.
コンパイルも割と速いし,実行速度は言うまでもなく速い.
クラスがないとか,例外がないとか,色々と言いたいことはあるでしょう.思想的には結構独特ですが,なれると普通に書けます.
今日はそんな中でテストの話をしようと思う.
やっぱりBDDがいい
golangにはテストをするための,testingというパッケージがある.
使い方はこの辺の記事が参考になるでしょう.
assertがないというのが,結構主題かもしれない.
ただ,使い方を見てみればわかる通り,これはExampleのテストでしかない.
個人的な意見なのだけれど,やっぱりパターンが複雑になればなるほど,分岐が多くなり,前準備が複雑になる.
どうしてもBDDを導入したかったので,ginkgoを入れてみた.
matcherにはgomegaというものが使われている.
使い方は説明を読んでもらえればわかるだろう.
BeforeEach
やAfterEach
のようなコールバックも用意していて,使いやすい.
BeforeEach
やAfterEach
は各It
節の直前・直後で実行されるが,BeforeSuite
などはtest_suiteが実行される直前に呼ばれるだけである.
テスト用のDBを切り替える
前提
さて,ginkgoの準備はできた.
golangは必ずしもwebサービスを作るものではないが,とりあえずwebサーバーで,裏側にDBを持っているようなものを想定しよう.
その場合,テスト用のDBをどうするかというのが結構な問題になる.
たとえば,Railsのようなフレームワークであれば,database.ymlの設定を,Railsが勝手に読み込んでくれる.
だから,テスト時には接続されるDBが自動的にテスト用のDBになる.
通常開発しているDBをテスト用に使ってしまっても構わないが,やっぱりデータをクリアするのは面倒だし,なによりテストしながら開発することを考えると,入れておいたデータが消えてしまうのは嫌だ.
開発用のデータというのは,できるだけ低コストで準備したいから,テスト毎に消されるなんて面倒が増えてしょうがない.
というわけで,テスト用のDBに切り替えられるようにしよう.
DBの接続設定を書く
まず,DBに接続している部分のコードを見てみよう.
たぶん,
import ( "database/sql" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "root:hogehoge@/sample_project?charset=utf8") if err != nil { panic(err.Error()) } }
こんなコードがかいてあるんじゃないだろうか.
ここを,まず環境変数によって切り替えられるようにする.
package db import ( "os" "database/sql" _ "github.com/go-sql-driver/mysql" ) func Database() *sql.DB { username := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") database := os.Getenv("DB_NAME") db, err := sql.Open("mysql", username + ":" + password + "@/" + database + "?charset=utf8") if err != nil { panic(err.Error()) } return db }
テストのBefore節で環境変数を書き換える
上記の関数を使ってDB接続する限り,接続先は環境変数に記述されているものとなる.
つまり,環境変数で与えられるDB_NAME
さえ書き換えてしまえば,ローカルの好きなDBにつなぐことができる.
ただし,環境変数そのものを入れ替えるので,テスト実行中に別のプロセスで開発したりしていないことを前提としている.
model_test.goにて,
package model_test import ( "os" "database/sql" "./database" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) var _ = Describe("List", func() { var ( currentdb string table *sql.DB ) BeforeEach(func() { testdb := os.Getenv("DB_TEST_NAME") currentdb = os.Getenv("DB_NAME") os.Setenv("DB_NAME", testdb) }) AfterEach(func() { table = database.Database() table.Exec("truncate users;") table.Close() os.Setenv("DB_NAME", currentdb) }) }
こんな感じにしておく.
これで環境変数としてDB_TEST_NAME
にテスト用DBの名前を入れておけば大丈夫.
ちゃんと終わったら元の値に戻しているので,終わったら普段通りに開発用DBに繋がるようになっている.
ちなみに,このコードは,DBの接続を切り替えるだけなので,ちゃんとCREATE DATABASEや,必要なテーブルのmigrationは終わらせてある前提.
そのへんはgooseあたりが使いやすかった.
mockとstub
もう一点,かなり悩ましい項目の一つにStubやMockがある.
structに包まれて,interfaceとして実装している,いわゆるインスタンスメソッド的な立ち位置の関数は,実はinterfaceごとを上書きしてやればmockできる.
こんな感じで.
そこまでしなくても,testifyを使うと,mockというpackageがあったりする.
ただ,どちらもstructのメンバー関数しか上書きできない.
グローバル関数をstubしたいときはどうするのか?
いいやり方はこのくらいしか見つからなかった.
csrf = checkCSRF func checkCSRF() bool { // hogehoge }
まぁこんな感じで,関数ポインタを使うように本体の実装を書き換える必要がある.
で,テスト側では
JustBeforeEach(func() { csrf = func() bool { return true } } // hogehoge
こうしてグローバル変数に入った関数ポインタを書き換えてやる.
もしテスト中にstubを解除したければ,関数ポインタの値を適当な変数に入れて持っておけば良さそう.
まだこのくらいしか書いていない
テストっていうのは状況を準備するのが大変だ.
これだけの複雑な状況をテストのたびに作りなおすのが結構だるくて….
RSpecでいうところのfactory_girlみたいなものも,探して,確かにあったんだけど,それじゃないような……感.github.com
ActiveRecordみたいなものをもたずに,自分でDBから必要なデータを取ってきて,オブジェクトを組み立てているので,factory_girlでデータそのものを作ってもらっても,結局組み立て部分のコードがかさむ…….
というところで,まだまだ悩み中です.
ただ,golangはgodocがかなり充実していて,適当なライブラリであってもREADMEよりgodocに詳しく書いてあって,ドキュメントに関してはあまり困らなくて,すごく嬉しい.