Golang におけるサブテストの並行処理実装について

Go1.7 が約1ヶ月前にリリースされましたが、つい先日 Go1.7.1 もリリースされ Golang の盛り上がりを感じますね!
さて、今回は Go1.7 から追加された機能の一つ「サブテスト」について紹介したいと思います。

サブテストとサブベンチマーク

Go1.7 から追加された、テスト(ベンチマーク)を効率的に記述&実行するための機能で、煩雑になってしまうテストコードを簡潔に記述することができます。
また、サブテストを並行で実行することも可能なため、効率的にテストを実行することも可能です。

テストコード

テーブル駆動テスト – Table-Driven Testing

Go1.7 以前から Golang ではテーブル駆動テストでテストコードを記述することが可読性や保守性の面で良いとされています。

func TestFoo(t *testing.T) {

	candidates := []struct {
		input    string
		expected string
	}{
		{"foo", "FOO"},
		{"bar", "BAR"},
	}

	for _, c := range candidates {
		result := strings.ToUpper(c.input)

		if c.expected != result {
			t.Errorf("expected %v, but %v", c.expected, result)
		}
	}
}

上記の例のように候補となるデータ構造の配列を用意しておき、その候補となるデータをイテレートしてテストをすることを「テーブル駆動テスト」といいます。

このテーブル駆動テストとサブテストを組み合わせることによって効率的にテストを記述することができます。

サブテスト – Subtests

サブテストをテストコードで使用することにより、テストを階層的に記述することができます。

func TestFoo(t *testing.T) {

	// setup code

	t.Run("A", func(t *testing.T) {

		// setup code

		t.Run("B", func(t *testing.T) {
			// ...
		})

		t.Run("C", func(t *testing.T) {
			// ...
		})

		// teardown code
	})

	// teardown code
}

階層化されたテストにより setup や teardown が “気持ち” 見やすくなった気がします。

setup, teardown コード

ちょっとした Tips ですが、テストコードの事前処理と事後処理としてそれぞれ setup と teardown の関数を用意することが多いですが、ほとんどのケースで setup と teardown はセットとなっているため、一つの関数で定義しておくと見やすいです。

func setupWithTeardown() func() {

	// setup code

	return func() {
		// teardown code
	}
}

// ケース1
func TestFoo(t *testing.T) {

	// do setup
	teardown := setupWithTeardown()

	t.Run("A", func(t *testing.T) {
		// ...
	})

	// do teardown
	teardown()
}

// ケース2
func TestBar(t *testing.T) {

	// do setup and then stacking teardown func
	defer setupWithTeardown()()
	
	t.Run("A", func(t *testing.T) {
		// ...
	})
}

ケース1のパターンは丁寧に処理の前後に関数を呼び出していて、ケース2のパターンでは setup を実行してすぐにコールスタックに teardown 関数をスタックさせています。(defer 実行させています)

サブテストの並行処理

サブテストでは t.Run 関数内で t.Parallel をコールすることによりテストを並行で処理させることができ、全てのサブテストの処理が完了し次第、親の(サブ)テストを終了することができます。

func TestFoo(t *testing.T) {
	// ...

	for _, c := range candidates {
		c := c // 変数のキャプチャ

		t.Run(c.name, func(t *testing.T) {
			t.Parallel() // 並行処理実行

			result := strings.ToUpper(c.input)

			if c.expected != result {
				t.Errorf("expected %v, but %v", c.expected, result)
			}
		})
	}
}

上記の場合、全てのサブテストが並行で処理されますが、teardown を実行させる場合は下記のように t.Run で囲んでおく必要があります。

func setupWithTeardown() func() {

	// setup code

	return func() {
		// teardown code
	}
}

// teardown をうまく効かせるために t.Run で囲む
func TestFoo(t *testing.T) {
	// ...

	// setup
	teardown := setupWithTeardown()

	t.Run("parent group", func(t *testing.T) {
		for _, c := range candidates {
			c := c

			t.Run(c.name, func(t *testing.T) {
				t.Parallel() // 並行処理実行

				result := strings.ToUpper(c.input)

				if c.expected != result {
					t.Errorf("expected %v, but %v", c.expected, result)
				}
			})
		}
	})

	teardown()
}

// teardown がうまく効かない例
func TestFooBar(t *testing.T) {
	// ...

	// setup
	teardown := setupWithTeardown()

	for _, c := range candidates {
		c := c

		t.Run(c.name, func(t *testing.T) {
			t.Parallel() // 並行処理実行

			result := strings.ToUpper(c.input)

			if c.expected != result {
				t.Errorf("expected %v, but %v", c.expected, result)
			}
		})
	}

	teardown() // この teardown はテスト実行中に処理されてしまう
}

defer をしても同じなので、並行処理させるサブテストで teardown を実行する場合は気をつける必要があります。

おわりに

以上がサブテストになります。今回はサブテスト中心に紹介しましたが、同じような記述でサブベンチマークにも展開することができます。
サブテストを使用することにより何かが大きく変わるわけではないですが、テストコードの可読性と保守性は高いほどメンテナブルなテストコードになるので、メンテナンスのしやすいテストコードにするためにも積極的に使用していきたいなと感じます。

参考リンク

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

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

Recommend

Swiftならこう書くシリーズ 10選

regexpとの付き合い方 〜 Go言語標準の正規表現ライブラリのパフォーマンスとアルゴリズム〜