emahiro/b.log

Drastically Repeat Yourself !!!!

テストの中で使うダミーデータを作成する

前提

を使ったケースにおいてデータベースのアクセスする実装のテストを書くときにダミーデータをセットアップする方法を記載します。

今回は追加でテストツールとして testerator を使っています。 testerator: testerator - GoDoc

愚直にデータを作って、消す

ベタな方法です。単体テストの内部でデータを作成してそのテストケースが終わったら削除して次のテストケースでもう一回作ります。

type Data struct {
    ID   int64  `datastore:"-"`
    Name string `datastore: "Name"`
}

func TestMethod (t *testing.T) {
    tests := []struct{
        name  string
        src   *Data
        want  string
    }{
        { name: "case_1", src: &Data{name: "alice"}, want: "bob"},
        { name: "case_1", src: &Data{name: "hoge"}, want: "fuga"},
    }
    
    _, ctx, _ := testerator.SpinUp()
    defer testerator.SpinDown()

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.go) {
            k := datastore.NewIncompleteKey("SampleKind")
            kk, err := datastore.Put(ctx, k, tt.src)
            if err != nil {
                panic(err)
            }

            // 何かしらのテスト

            // テスト終了後にput時に返されたkeyを指定して削除する。
            if err := datastore.Delete(ctx, kk); err != nil {
                panic(err)
            }
        })
    } 
}

特に何も考えずにやるならこの方法かなと思います。DeleteするのはPutしたときに帰ってきたCreateされたレコードのDatastoreのKeyを使うことくらいかと思います。

ここでは純粋なDatastoreのパッケージ使いましたが、普段は業務ではgoon使ってるのでデータの作成と削除はgoonのPutとDeleteを使ってます。

テストケースごとにSpinDownする

これがもう一つ考えたのが、テストケース回すごとにInstance消せばdatastore丸ごとリセットできるんじゃないかと思って以下のようなパターンです。 上記のテストごとに作って消すでもいいと思ったんですが、常にフレッシュな状態のインスタンスでテストしたいケースとかあると思うのでテストケースごとにappengineのインスタンスを消すパターンを考えました。

testerator内のdatastoreやmemcacheをimportしておくと、SpinDown() ごとにdatastore、memcacheを丸ごと消してくれるのでテストケースごとに SpinDown() させます。

import (
    // do testerator feature setup
    _ "github.com/favclip/testerator/datastore"
    _ "github.com/favclip/testerator/search"
    _ "github.com/favclip/testerator/memcache"
)

# 略

type Data struct {
    ID   int64  `datastore:"-"`
    Name string `datastore: "Name"`
}

func TestMethod (t *testing.T) {
    tests := []struct{
        name  string
        src   *Data
        want  string
    }{
        { name: "case_1", src: &Data{name: "alice"}, want: "bob"},
        { name: "case_1", src: &Data{name: "hoge"}, want: "fuga"},
    }
    
    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.go) {
            _, ctx, _ := testerator.SpinUp()
            defer testerator.SpinDown() // func(t *testing.T) ごとに呼ばれる。
         
            k := datastore.NewIncompleteKey("SampleKind")
            kk, err := datastore.Put(ctx, k, tt.src)
            if err != nil {
                panic(err)
            }

            // 何かしらのテスト
        })
    } 
}

この方法、綺麗なインスタンスの状態が欲しいときだったり、Context引き回したくない場合にはいいかなと思いますが、テストごとにインスタンス立ち上げるコストがかかるので多分テスト遅くなります。

追記

testerator.SpinDownは、起動しているインスタンス数の残数が0の時はインスタンスをそのまま落としますが、1つ以上残数が残っているとインスタンスを落とさずに状態をcleanupのみしてくれます。

SpinDown dev server.

This function clean up dev server environment. However, internally there are two types of processing. #1. if internal counter == 0, spin down dev server simply. #2. otherwise, call each DefaultSetup.Cleaners. usually, it means cleanup Datastore and Search APIs. see document for SpinUp function.

refs: https://godoc.org/github.com/favclip/testerator#SpinDown

そのため テストケースごとにSpinDownする 場合のときは TestMainで先に testerator.SpinUp しておく良いです。そうすると、forの中でインスタンスのSpinUp/Down を繰り返しても、常にインスタンスの残機が1つ以上残ってる状態なのでインスタンスを落とさずにDatastoreの状態をCleanupしてくれて、毎回インスタンスを起動し直さなくても、まっさら状態のインスタンスを使えて、かつテストが高速化できます。

残機のカウントの実装は以下にあります。
refs: https://github.com/favclip/testerator/blob/master/testarator.go#L164-L175

まとめ

ケースバイケースだと思いますが、多分都度作って消す方がコスト安いし良さそうです。(個人の主観です。) TestMainの実装をするのであれば、テストケースごとに SpinUp/Down をする場合もアリかなと思います。