読者です 読者をやめる 読者になる 読者になる

golangでテストをする

最近golangを結構書いています.
コンパイルも割と速いし,実行速度は言うまでもなく速い.

クラスがないとか,例外がないとか,色々と言いたいことはあるでしょう.思想的には結構独特ですが,なれると普通に書けます.

今日はそんな中でテストの話をしようと思う.

やっぱりBDDがいい

golangにはテストをするための,testingというパッケージがある.

golang.jp


使い方はこの辺の記事が参考になるでしょう.

qiita.com



assertがないというのが,結構主題かもしれない.

ただ,使い方を見てみればわかる通り,これはExampleのテストでしかない.

個人的な意見なのだけれど,やっぱりパターンが複雑になればなるほど,分岐が多くなり,前準備が複雑になる.
どうしてもBDDを導入したかったので,ginkgoを入れてみた.

github.com



matcherにはgomegaというものが使われている.

github.com




使い方は説明を読んでもらえればわかるだろう.

BeforeEachAfterEachのようなコールバックも用意していて,使いやすい.

BeforeEachAfterEachは各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
}


これで環境変数からDB接続情報が取れるようになった.
direnvあたりを使って環境変数を管理しておくと楽だろう.

テストの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あたりが使いやすかった.

liamstask / goose — Bitbucket




mockとstub

もう一点,かなり悩ましい項目の一つにStubやMockがある.


structに包まれて,interfaceとして実装している,いわゆるインスタンスメソッド的な立ち位置の関数は,実はinterfaceごとを上書きしてやればmockできる.

こんな感じで.

qiita.com



そこまでしなくても,testifyを使うと,mockというpackageがあったりする.

github.com



ただ,どちらもstructのメンバー関数しか上書きできない.


グローバル関数をstubしたいときはどうするのか?


いいやり方はこのくらいしか見つからなかった.

matope.hatenablog.com


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に詳しく書いてあって,ドキュメントに関してはあまり困らなくて,すごく嬉しい.