30
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

and factoryAdvent Calendar 2020

Day 21

golangテストで使えるライブラリ覚え書き

Last updated at Posted at 2020-12-20

この記事はand factory Advent Calendar 2020の21日目の記事です。
昨日は@tsumuchanさんの「【Android】独自のConstraintHelperを作成し、MotionSceneをちょっとスッキリさせてみる」でした。

はじめに

Go Conference'20 in Autumn SENDAIで発表された「DRY & 型安全にテスト用structを初期化しよう」を見た際、「そういえばテストで使われてそうなライブラリとか何も知らんな」と思ったので、せめて有名どころや発表内で紹介されてたものは知っておきたい…ということで調べて試してみました。
雰囲気で & 愚直にテストを書いている自覚があるので、既存のライブラリ等を知って少しでも効率的にできればなと思った次第です。
同じように雰囲気でテスト書いてる方の助力になれれば幸いです。

go test

言わずと知れたGoのテスト用標準ライブラリ。
こちらに関しては使い方含めていろいろな記事があるでしょうし、説明は省きます。

テストの雛形を作るのが手作業だとまずダルいのですが、VSCodeであればコマンドパレットからGenerate Unit Tests For ...を実行してテストコードの雛形を作成することができます。
スクリーンショット 2020-12-19 22.43.46.png
関数・ファイル単位の雛形であればコードを開いて右クリックでも雛形作成のメニューが出てきます。

書いてて気づいたんですがカバレッジを取れるコマンド(Toggle Test Coverage In Current Package)もあるんですね。
スクリーンショット 2020-12-19 23.18.34.png
テストが書けていないところは赤色になるので、どこの分岐のテストが足りてないかを手元で確認するときなどに使えそうです。

google/go-cmp

Google非公式の値比較のライブラリ。
標準の比較機能であるreflect.DeepEqualを使用すれば構造体の比較等も行えるのですが、unexportedなフィールドやTime型のフィールドが一致せずテストがPASSしないといった問題点があります。go-cmpを使用することでより柔軟な比較を行うことができます。

使用方法としては以下のような感じ。

package main

import (
	"github.com/google/go-cmp/cmp"
)

func Test_getResults(t *testing.T) {
	tests := []struct {
		name string
		want TestStruct
	}{
		{
			name: "test_1",
			want: TestStruct{
				Name:      "Test",
				Numbers:   []int{1, 2, 3, 4, 5},
				CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := getResults()
			if diff := cmp.Diff(got, tt.want); diff != "" {
				t.Errorf(diff)
			}
		})
	}
}

cmp.Diff()に比較したい構造体を渡せば良いだけです。差異がない場合は空文字が返ってきます。

差異がある場合は以下のような結果が返却されます。異なっている部分には+-がつくので、reflect.DeepEqualの結果よりも直感的に見やすいです。

=== RUN   Test_getResults/test_1
    test_test.go:28:   main.TestStruct{
                Name: "Test",
                Numbers: []int{
                        ... // 2 identical elements
                        3,
                        4,
        -               5,
        +               6,
                },
                CreatedAt: s"2020-01-01 00:00:00 +0000 UTC",
          }
--- FAIL: Test_getResults (0.00s)

go-cmpではオプションが設定でき、特定のフィールドの比較を無視したり、unexportedなフィールドの比較をON/OFFできたりと、用途に合わせてカスタマイズすることができます。
オプションについては「go-cmpを使う理由とTipsの紹介」で読みやすく、必要なものがまとめられていました。

go mock

インタフェースのmockを作るためのライブラリ。

インストールは

$ go get github.com/golang/mock/gomock
$ go get github.com/golang/mock/mockgen

で行い、以下のようにインタフェースに対してコマンドを実行すると、モックを作成してくれます。

$ mockgen -source sample.go

インストール方法から使い方まで、「Go Mockでインタフェースのモックを作ってテストする #golang」で分かりやすくまとめられています。

testfixutures

yamlファイルからDBのデータを作成することで、テスト前にDBのクリーンアップ & テストデータのロードを行えるライブラリ。
Ruby on Railsのフィクスチャ(サンプルデータ)がyaml形式のファイルとして保存されていることから発想を得たそうです。yamlファイルにサンプルデータを記述しておくことで、特定のDBに依存しないデータを用意することができます。

使用する場合はまず任意のディレクトリにyamlファイルを用意します。

testdata/fixtures/greetings.yml
- id: 1
  msg: Good Morning
  created_at: RAW=NOW()
  updated_at: RAW=NOW()
  deleted_at: NULL

1テーブルごとに1ファイル用意する必要があり、ファイル名はテーブルの名前をつける必要があります。
今回はgreetingsというテーブルにデータを入れますので、greetings.ymlというサンプルデータをtestdata/fixturesディレクトリ配下に用意しました。
NOW()などの関数やSQL文を使いたい場合はRAW=を指定、NULLを入れたい場合はNULLと指定すればいい感じに値を突っ込んでくれるようです。

テスト実行時には、以下のようにymlファイルを読み込んでデータをテーブルに突っ込みます。

package main

import (
	_ "github.com/go-sql-driver/mysql"

	"github.com/go-testfixtures/testfixtures/v3"
)

func Test_getGreeting(t *testing.T) {
	type args struct {
		t time.Time
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		// Some tests
	}

	db, err := sql.Open("mysql", "root:@/test")
	if err != nil {
		fmt.Println(err)
	}
	defer db.Close()

	fixtures, err := testfixtures.New(
		testfixtures.Database(db),
		testfixtures.Dialect("mysql"),
		testfixtures.Directory("testdata/fixtures"),
	)
	if err != nil {
		fmt.Println(err)
	}

	for _, tt := range tests {
		if err := fixtures.Load(); err != nil {
			fmt.Println(err)
		}

		t.Run(tt.name, func(t *testing.T) {
			// Run tests
		})
	}
}

DBへの接続を開いて、testfixtures.New()でDBへのコネクション、DBの種類、サンプルデータが入っているディレクトリを指定します。今回はディレクトリでサンプルデータを読み込んでいますが、特定のファイルのみを読み込むなどの調整もできるようです。
fixtures.Load()でテーブルからのデータの削除 & テーブルへのデータの追加を行ってくれるので、特段テストごとにデータを変える必要がなければ各テストの前処理として実行するとよさそうです。

factory-go

複雑な構造体を簡単に作成するためのライブラリ。
構造体を作るファクトリを定義することで柔軟にデータが作れるので、構造体の構成が変わっちゃったりしたときにもメンテナンスを容易にできます。pythonにfactory_boyというライブラリがあり、そこから着想を得たそう。

package main

import (
	"github.com/Pallinder/go-randomdata"
	"github.com/bluele/factory-go/factory"
)

type User struct {
	ID       int
	Name     string
	Location string
}

// 'Location: "Tokyo"' is default value.
var UserFactory = factory.NewFactory(
	&User{Location: "Tokyo"},
).SeqInt("ID", func(n int) (interface{}, error) {
	return n, nil
}).Attr("Name", func(args factory.Args) (interface{}, error) {
	user := args.Instance().(*User)
	return fmt.Sprintf("user-%d", user.ID), nil
})

func main() {
	for i := 0; i < 3; i++ {
		user := UserFactory.MustCreate().(*User)		
		fmt.Println("ID:", user.ID, " Name:", user.Name, " Location:", user.Location)
	}
}

テストで使用する構造体を定義して、factory.NewFactory()でその中身を作るための式を定義する、という感じのようです。
for文の中でMustCreate()を使って構造体を生成すれば、自動的にIDなどがセットされていきます。上記のLocationのように固定値を入れたりすることもできます。
ちなみにファクトリを使って作る構造体にはIDが必須なようで、ないとエラーが出ます。基本的にはIDにセットされた値を使って各フィールドの値を作っていくような書き方になりそう。

使ってみた感じちょっと可読性が低い感じがしますね。。
構造体の中に構造体が入っているようなものやフィールド数が多いものを作ろうとしたり、IDに関係ない値をセットしたい(IDが1だったらLocationがTokyo、2だったらChinaとか)となると、結構複雑になってしんどいかも。

おわりに

業務でテストを書くことがありますが、今でも結構testingパッケージの機能を把握しきれていなかったり、テストケースをひたすら書いていると面倒臭くなったり、途中で放置してどこまで書いたんだか分からなくなったりしてます。。

今回調査したのは有名どころばかりという感じですが、他のライブラリにも目を光らせつつ、適宜取り入れてなんとか楽ができればいいな〜!と思います。

明日は@cpp0302さんです!

参考

30
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?