2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スタンバイAdvent Calendar 2024

Day 20

goのテスト関連で最近まで知らなかったことをまとめてみた

Last updated at Posted at 2024-12-19

はじめに

この記事は スタンバイ Advent Calendar 2024 の20日目の記事です。
昨日の記事は @taca10 さんの「toolchainの挙動について学んだ」でした。

ごあいさつ

株式会社スタンバイで求人取り込み部分のエンジニアをやっております、本田と申します!
スタンバイでは、ScalaからGoへのリプレイスやFlinkを用いて大量の求人のストリーム処理などを行っています!
この一年リプレイスや新規開発でGoと触れ合う機会が増え、中でもテスト関連について最近まで知らなかったことがあったのでアドベントカレンダーに参加させていただきました!

goのテスト関連で最近まで知らなかったこと

TestMainで処理制御できる

結合テストなどのユースケースで、実行前にテーブルレコード追加などをおこなってからテストして、その後削除したいたいなど状態が変わるものについてテストする時にTestMainがよく使われます。

func TestMain(m *testing.M) {
	setup() // DBのレコード追加などの事前処理
	m.Run() // ここでテストケースが実行される
    teardown() // テーブルの削除などの事後処理
}

func TestAdd(t *testing.T) {
  ....
}
...

全体のテストが実行される前後でこのように処理を入れることができます。
Google検索結果で様々なサンプルがありますが、Go 1.15からはos.Exitが必要なくなっています

テストの並列実行

t.Parallel()をテストコード内に記述すると、テストケースの並列実行を行ってくれます。

func TestXXX(t *testing.T) {
	t.Parallel()
  
	t.Run("case1", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
	t.Run("case2", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
  }

この場合はサブテスト(t.Runで記述している)とテスト関数が並列実行されます。

また、適切な場所にt.Parallel()がいるかどうかは
https://golangci-lint.run/usage/linters/#paralleltest
golangci-lintなどで静的解析をしてくれるので、テストの実行速度に問題がある場合(例えばテストコードにスリープを入れなければならなく待たないといけない場合に他のテストケースは実行しておいて欲しいなど)に試してみてもいいかもしれないです。

並列化していると下記のように並列実行したケースの終了を待たずにdefer処理が動き出す可能性があります。
そのときはt.Cleanupをつかって事後処理を宣言した方が良いです。

func TestXXX(t *testing.T) {
	t.Parallel()
    defer teardown() <- ここの処理がt.Parallel()の結果を待たずして実行される
  
	t.Run("case1", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
	t.Run("case2", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
  }
func TestXXX(t *testing.T) {
	t.Parallel()
    t.Cleanup(func() {
        teardown()
    }
  
	t.Run("case1", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
	t.Run("case2", func(t *testing.T) {
	  t.Parallel()
	  ...
	})
  }

どの場合にdeferを使っていいのかはメルカリさんのテックブログ記事があったので引用すると以下のようになります。

・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数を含んでいなければ、defer文あるいはt.Cleanup()メソッドのどちらで後処理を記述してもよい。
・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数は含んでいて、そのすべてのサブテスト関数がt.Parallel()メソッドを呼び出していない場合、defer文あるいはt.Cleanup()メソッドのどちらで後処理を記述してもよい。
・トップレベルのテスト関数が、t.Run()メソッドによるサブテスト関数を含んでいて、少なくともその一つのサブテスト関数がt.Parallel()メソッドを呼び出している場合、t.Cleanup()メソッドで後処理を記述する。

構造体の比較

構造体の比較は下記のようにreflection.DeepEqualを使えば良いのはわかっていましたが

package main

import (
	"reflect"
	"testing"
)

func TestUser(t *testing.T) {
	expect := User{"test", 31, "male"}
	actual := User{"test", 30, "male"}

	if !reflect.DeepEqual(expect, actual) {
		t.Errorf("want %v got %v", expect, actual)
	}
}
$ go test
--- FAIL: TestUser (0.00s)
    main_test.go:13: want {test 31 male} got {test 30 male}
FAIL
exit status 1
FAIL	ftpsample	0.227s

まず、どこの構造体の要素が異なっているか、このケースだと値しかなくてパッと見どこのフィールドが違っているか特定するのに時間がかかります。
まず、細かいですが、%v -> %#vにすることで、どのフィールドの値なのかがわかるようになって可読性が高いです。

$ go test
--- FAIL: TestUser (0.00s)
    main_test.go:13: want main.User{Name:"test", Age:31, Gender:"male"} got main.User{Name:"test", Age:30, Gender:"male"}
FAIL
exit status 1
FAIL	ftpsample	0.171s

今のままだと目視でどこが間違っているか確認する必要があり、差分で出したくなってきます。
構造体の差分(どこが間違っているか)を出したい場合はgo-cmpを使うと差分をきれいに出してくれるようになります。

package main

import (
	"testing"

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

func TestUser(t *testing.T) {
	expect := User{"test", 31, "male"}
	actual := User{"test", 30, "male"}
	diff := cmp.Diff(expect, actual)
	if diff != "" {
		t.Errorf("(-expected, +result)\n%s", diff)
	}
}
$ go test
--- FAIL: TestUser (0.00s)
    main_test.go:16: (-expected, +result)
          main.User{
          	Name:   "test",
        - 	Age:    31,
        + 	Age:    30,
          	Gender: "male",
          }
FAIL
exit status 1
FAIL	ftpsample	0.305s

ただ、上記のようにエラー文言をカスタマイズするのも面倒というのであればtestifyでも同じようなことができる且つ自前でPrintfを用意しなくてもきれいに差分を出してくれます。

package main

import (
	"testing"

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

func TestUser(t *testing.T) {
	expect := User{"test", 31, "male"}
	actual := User{"test", 31, "female"}

	assert.Equal(t, expect, actual)
}
$ go test
--- FAIL: TestUser (0.00s)
    main_test.go:13:
        	Error Trace:	/Users/arata.honda/stanby/gosample/main_test.go:13
        	Error:      	Not equal:
        	            	expected: main.User{Name:"test", Age:31, Gender:"male"}
        	            	actual  : main.User{Name:"test", Age:31, Gender:"female"}

        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -3,3 +3,3 @@
        	            	  Age: (int32) 31,
        	            	- Gender: (string) (len=4) "male"
        	            	+ Gender: (string) (len=6) "female"
        	            	 }
        	Test:       	TestUser
FAIL
exit status 1
FAIL	ftpsample	0.443s

みやすいと言う観点だと、testifyかなと少し思いますが、「CreatedAtみたいに動的に変わるフィールドをもつ構造体を比較する時に部分一致したい...!!」などの柔軟な比較のオプションなどを行う時はgo-cmpの方が良いみたいで使い分けても良いと思いました。(参考記事: https://sy-tencho.com/posts/go-compare-structs/)

また、go-cmpを使うとprotocol bufferで生成した構造体に関しても比較ができるようになっています。

package main

import (
	"ftpsample/internal"
	"testing"

	"github.com/google/go-cmp/cmp"
	"google.golang.org/protobuf/testing/protocmp"
)

func TestProtoJob(t *testing.T) {
	expect := internal.Job{JobId: "test", Description: "desc"}
	actual := internal.Job{JobId: "test", Description: "desc12"}

	diff := cmp.Diff(&expect, &actual, protocmp.Transform())
	if diff != "" {
		t.Errorf("(-expected, +result)\n%s", diff)
	}
}

protoの生成された構造体の中にフィールドでMessageがあり、refrection.DeepEqualやcmp.Diffを単純に行うとパニックになるケースがあります。(protoの公式ドキュメント)
Diffのオプションにproto.Transform()を渡すことでMessageをmap[string]anyに変換してくれるcmpのOptionを提供してくれます。

また、Diffに各構造体の参照を渡してやる(&expect)と、Diffの中でprotoのShallow copyにならずgo vetで引っ掛からなくなります。issue

$ go test
--- FAIL: TestProtoJob (0.00s)
    main_test.go:17: (-expected, +result)
          (*internal.Job)(Inverse(protocmp.Transform, protocmp.Message{
          	"@type":       s"Job",
        - 	"description": string("desc"),
        + 	"description": string("desc12"),
          	"jobId":       string("test"),
          }))
FAIL
exit status 1
FAIL	ftpsample	0.298s

感想

現状の開発でprotocol bufferを使った構造体を比較するときにどうすればもっとわかりやすい差分比較ができるだろうと思ってこの度記事を書かせてもらいましたが意外とテスト関連で知らないことは多く、これからもどんどん知識を深めていければと思いました!

明日は@mick-azの記事がでるそうです!
楽しみですね!

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?