突撃!隣のGo言語テストコード(エウレカのGo言語におけるテストコードの工夫)

こんにちは、エンジニアの田野です。
ダイエットのために通いはじめたボクシングジムにて、本格的にガードの甘さを指摘され、どんどん練習がハードになりつつある毎日です。(楽しくなってきました)

さて、エウレカではpairsのGo言語フルスクラッチプロジェクトに取り組んでいます。この記事ではエウレカでGo言語のテストコードにどのような工夫をしているのか、トピックごとに紹介させていただきます。

アサーションについて

そもそもGo言語ではアサーションを提供していないわけですが、プロジェクトメンバー各人にプログラムの確認ポイントを明示してもらうため、エウレカのGo言語プロジェクトではアサーションを利用することに決めました。

参考:Go の Test に対する考え方 – Qiita

Ginkgo / Gomega

初期の頃はGinkgo/Gomega を採用し、RSpec風なテストコードを施行していました。ですがテスト量が増えるに従ってGinkgoのテスト実行までがみるみる遅くなっていきました。(当時)

テストの趣旨をDescribeを使って明記できないのは残念なのですが、代わりにtestifyを採用することとなりました。
先日のGo Conference 2015 Winterでお会いした方々もアサーションを使うときは、testifyを採用している人が多かったので、testifyはアサーション派にとって割とスタンダードになりつつあるものと感じています。

ちなみにtestifyのアサーションはこのような書き方になるのですが、アサーションの量が増えると、このtを毎回つけるのが面倒だったりします。

package sample_test

import "github.com/stretchr/testify/assert"

func TestSampleFunc(t *testing.T) {

assert.True(t, true)
}

これはテストの初段階でassert.New(t)assertとして定義することで簡略化できます。

package sample_test

// サンプルコードです。
import "github.com/stretchr/testify/assert"

func TestSampleFunc(t *testing.T) {
var assert = assert.New(t)

assert.True(true) // t の指定が要らなくなりました
}

fixture問題

テストをするといえば、データベースの状態を固定するためにfixtureを使います。yamlの内容をデータベーステーブル構造体へ代入、ORMの機能で保存する機構をつくりました。コードはテストなので、ラフにpanic()を打っています。

※ORMとしてxormを利用しています。サンプルコードは一部加工しています。

// SetupFixture loads data from yaml to database
// Fixtureのロード
func SetupFixture(ymlPath string, struc interface{}) {
buf, err := ioutil.ReadFile(ymlPath)
if err != nil {
panic(err)
}

var fixtures map[string]map[string]interface{}
yaml.Unmarshal(buf, &fixtures)

// 初期化用変数
emptyJSON, _ := json.Marshal(reflect.ValueOf(struc).Elem().Interface())

for _, fixture := range fixtures {

// 初期化
err = json.Unmarshal(emptyJSON, struc)
if err != nil {
panic(err)
}

// 各ymlデータの割り当て
if err := mapToStruct(fixture, struc); err != nil {
panic(err)
}

db := // somecode for create connection //
_, err = db.NoAutoTime().Insert(struc)
if err != nil {
panic(err)
}
}
}

// http://play.golang.org/p/Kd7TRoRG5w
func mapToStruct(m map[string]interface{}, val interface{}) error {
tmp, err := json.Marshal(m)

if err != nil {
return err
}
err = json.Unmarshal(tmp, val)
if err != nil {
return err
}
return nil
}

変数strucは様々なデータベーステーブルを構造体として定義したものです。この構造体にはjsonアノテーションをつけます。(下記にサンプルを載せておきます)

ミソは2点で、

  • emptyJSON, _ := json.Marshal(reflect.ValueOf(struc).Elem().Interface())でreflectをつかうことで、さまざまなstrucの型に対応するように工夫しています。
  • mapToStruct関数で、json.Unmarshal(tmp, val)とやっていますが、Unmarshalが勝手に型にあわせて代入してくれます。キャストなどの操作が省略できます。

これを以下のようなyaml、データベーステーブルの構造体を用意して動かします。

# Samの情報fixture - some_yaml_path/person_sam.yml
Sam:
name: "Sam"
birthday: "2000-10-10T00:00:00Z"
// データベーステーブルの構造体
type Person struct {
Name     string    `json: name`
Birthday time.Time `json: birthday`
}

SetupFixtureはテストコードでこのように呼び出します。

// Person{} はデータベーステーブル Person の構造体
SetupFixture("some_yaml_path/person_sam.yml", &Person{})

これをTestHoge(t *testing.T)の最初で呼ぶことで、テストごとのfixtureの投入を実現しています。

<h2>時刻問題</h2>

テストで話題になりがちですが、時間に依存するテストで「現在時刻をどのようにズラすか」という問題があります。弊社のテストコードでは、<code>time.Now()</code>を排除して<code>env.TimeNow()</code>を使うようにルール化しました。

<code>env.TimeNow()</code>は以下の様なコードです。

// +build tests

package env

import "time"

// debugInfo is
type debugInfo struct {
debugTime *time.Time
}

// newDebugInfo ...
func newDebugInfo() debugInfo {
return debugInfo{}
}

var info debugInfo = newDebugInfo()

func SetDebugTime(t *time.Time) {
info.debugTime = t
}

func ResetDebugTime() {
info.debugTime = nil
}

func TimeNow() time.Time {
if info.debugTime != nil {
return *info.debugTime
}
return time.Now()
}

1行目の<code>// +build tests</code>でビルドタグを指定し、ビルドタグなしのプロダクションコードからは呼び出せないように施しています。よってテストは<code>go test -tags=tests</code>といったようにビルドタグ付きで動かすことになります。

<code>env.TimeNow()</code>が返却するコードは、<code>SetDebugTime()</code>で事前に指定されている場合は指定された日時、指定されていない場合は現在日時を返すようになっています。

<h3>ResetDebugTime()忘れる問題</h3>

しかしこのルールでやっているうち、ある日特定のテストコードが落ちるようになりました。テストの内容は正しいのになんでだろう・・・とハマったときに、ダンプしながらテストを見ると時刻が現在日時でないのです。そうです、タイトル通りですが前のテストが<code>defer ResetDebugTime()</code>を実行し忘れているのです。

ここで弊社の金子がこれを対策する関数を実装してくれました。

func TheWorld(dio func(), args ...interface{}) {
const defTime = "2009-11-10 23:00:00"
if len(args) == 0 {
TheWorld(dio, defTime, true)
return
} else {
switch t := args[0].(type) {
case bool:
TheWorld(dio, defTime, t)
return
case string, time.Time:
if len(args) == 1 {
TheWorld(dio, t, true)
return
}
isTheWorld := args[1].(bool)
if isTheWorld {
SetTimeParseDateTime(t)
}
dio()
ResetDebugTime()
}
}
}

おや・・!この関数名と変数名!何かの香りがしますねWRYYYY!この関数を実装していただいたことにより、テストはこんな感じになりました。

// +build tests

import (
"hogepath/env"
"github.com/stretchr/testify/assert"
)

func TestSampleFunc(t *testing.T) {
var assert = assert.New(t)

env.TheWorld( // これでくくります
func(){
assert.True(true)
},
"2014-12-25 03:21:11",
)

}

コードを見ていただければ一目瞭然で、これでResetDebugTime()のことを忘れられるだけでなく、なんか支配者になった気持ちになります。(WRYYYY言いたかっただけなんじゃないかという質問にはYESと答えます。)

おわりに

みなさんはテストコードにどのような工夫をされていますでしょうか? Twitterや、イベントでお会いさせていただいた際に、ぜひお話しさせてください。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

ClojureScript & Golangでチャットアプリを作ってみた

これだけ押さえておけば大丈夫!Webサービス向けVPCネットワークの設計指針