🐙

Go言語のサブテストや、テストケース前後の処理の書き方について

2024/06/10に公開

初めに

  • 概要: Go言語のテストコードで、サブテストやテストコード全体での前後処理、テストケース毎の前後処理を行う方法について説明します。
  • 背景: テストケースの量が多くなるにつれて可読性が悪くなったり、テスト前後に共通の処理を実行したかったため。

テスト対象コード

package main

import (
	"fmt"
	"strings"
)

// 引数の値から、"Hello {namesに設定されている値を", "区切り}."の文字列生成して返却
func Hello(names []string) (string, error) {
	if len(names) < 1 {
		return "", fmt.Errorf("names is empty")
	}

	return fmt.Sprintf("Hello %s.", strings.Join(names, ", ")), nil
}

実装例

サブテスト

  • 実装方法: t.Run("", func(t *testing.T)関数をテストケース毎に実装

実装例

package main

import (
	"testing"
)

// 正常系テスト
func TestPositive(t *testing.T) {
	t.Run("正常系01 パラメーター(names []string)のサイズ1", func(t *testing.T) {
		s, err := Hello([]string{"Jon"})
		if err != nil {
			t.Fatalf("err is not nil")
		}
		if s != "Hello Jon." {
			t.Fatalf("not expected string")
		}
	})
	t.Run("正常系02 パラメーター(names []string)のサイズ2", func(t *testing.T) {
		s, err := Hello([]string{"Jon", "Mary"})
		if err != nil {
			t.Fatalf("err is not nil")
		}
		if s != "Hello Jon, Mary." {
			t.Fatalf("not expected string")
		}
	})
}

// 異常系テスト
func TestNegative(t *testing.T) {
	t.Run("異常系01 パラメーター(names []string)のサイズ0", func(t *testing.T) {
		s, err := Hello([]string{})
		if err == nil {
			t.Fatalf("err is nil")
		}
		if s != "" {
			t.Fatalf("not expected string")
		}
	})
}

実行結果

% go test -v
=== RUN   TestPositive
=== RUN   TestPositive/正常系01_パラメーター(names_[]string)のサイズ1
=== RUN   TestPositive/正常系02_パラメーター(names_[]string)のサイズ2
--- PASS: TestPositive (0.00s)
    --- PASS: TestPositive/正常系01_パラメーター(names_[]string)のサイズ1 (0.00s)
    --- PASS: TestPositive/正常系02_パラメーター(names_[]string)のサイズ2 (0.00s)
=== RUN   TestNegative
=== RUN   TestNegative/異常系01_パラメーター(names_[]string)のサイズ0
--- PASS: TestNegative (0.00s)
    --- PASS: TestNegative/異常系01_パラメーター(names_[]string)のサイズ0 (0.00s)
PASS
ok      example 0.247s

よく見かけるテストコードの例

func TestHello(t *testing.T) {
	testCases := []struct {
		name           string
		input          []string
		expectedResult string
		expectedError  error
	}{
		{name: "正常系_01", input: []string{"Jon"}, expectedResult: "Hello Jon.", expectedError: nil},
		{name: "正常系_02", input: []string{"Jon", "Mary"}, expectedResult: "Hello Jon, Mary.", expectedError: nil},
		{name: "異常系_01", input: []string{}, expectedResult: "", expectedError: fmt.Errorf("names is empty")},
		// ...etc
	}

	// テストの実行
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// テストコード呼び出し、アサーション
		})
	}
}

後者のテストコードで記述していて個人的に気になる点は、テストケースを増やすたびに testCases のサイズが増えていくと可読性が悪くなるところ(struct{} のフィールドが多いとなおさら見づらくなってきます)と、テストコードを軽く見た時に、どのようなテストケースがあるか分かりづらいところです。
両者を使い分けてもよさそうです。

テストケース全体での前後処理

  • 使用例: データベースの接続、テスト全体で使用するテストデータの登録・削除、グローバル変数の初期化等
  • 実装方法: TestMain(m *testing.M)関数による共通処理の実装

実装例

package main

import (
	"fmt"
	"testing"
)

func TestMain(m *testing.M) {
	// テスト前に実施する処理
	setup()

	fmt.Println("Start test cases")
	m.Run()
	fmt.Println("End test cases")

	// テスト後に実施する処理
	teardown()
}

func setup() {
	fmt.Println("Start setup()")
}

func teardown() {
	fmt.Println("Start teardown()")
}

// 正常系テスト
func TestPositive(t *testing.T) {
	t.Run("正常系01 パラメーター(names []string)のサイズ1", func(t *testing.T) {
		s, err := Hello([]string{"Jon"})
		if err != nil {
			t.Fatalf("err is not nil")
		}
		if s != "Hello Jon." {
			t.Fatalf("not expected string")
		}
	})
	t.Run("正常系02 パラメーター(names []string)のサイズ2", func(t *testing.T) {
		s, err := Hello([]string{"Jon", "Mary"})
		if err != nil {
			t.Fatalf("err is not nil")
		}
		if s != "Hello Jon, Mary." {
			t.Fatalf("not expected string")
		}
	})
}

// 異常系テスト
func TestNegative(t *testing.T) {
	t.Run("異常系01 パラメーター(names []string)のサイズ0", func(t *testing.T) {
		s, err := Hello([]string{})
		if err == nil {
			t.Fatalf("err is nil")
		}
		if s != "" {
			t.Fatalf("not expected string")
		}
	})
}

実行結果

% go test -v
Start setup()
Start test cases
=== RUN   TestPositive
=== RUN   TestPositive/正常系01_パラメーター(names_[]string)のサイズ1
=== RUN   TestPositive/正常系02_パラメーター(names_[]string)のサイズ2
--- PASS: TestPositive (0.00s)
    --- PASS: TestPositive/正常系01_パラメーター(names_[]string)のサイズ1 (0.00s)
    --- PASS: TestPositive/正常系02_パラメーター(names_[]string)のサイズ2 (0.00s)
=== RUN   TestNegative
=== RUN   TestNegative/異常系01_パラメーター(names_[]string)のサイズ0
--- PASS: TestNegative (0.00s)
    --- PASS: TestNegative/異常系01_パラメーター(names_[]string)のサイズ0 (0.00s)
PASS
End test cases
Start teardown()
ok      example 0.154s

テストケース毎の前後処理

  • 使用例: テストで使用するテストデータの登録・削除、共通の初期化処理等
  • 実装方法: 各テストケース毎に先頭で処理を記述(呼び出し)し、defer で後処理を呼び出す(jestの beforeEach()、afterEach() に該当する機能は現在なさそうで、自前で忘れずに呼び出すしかないようでした)

実装例

package main

import (
	"fmt"
	"testing"
)

func beforeEach() {
	fmt.Println("Start beforeEach()")
}

func afterEach() {
	fmt.Println("Start afterEach()")
}

// 正常系テスト
func TestPositive(t *testing.T) {
	t.Run("正常系01 パラメーター(names []string)のサイズ1", func(t *testing.T) {
		beforeEach()      // テスト前処理
		defer afterEach() // テスト後処理

		s, err := Hello([]string{"Jon"})
		if err != nil {
			t.Fatalf("err is not nil")
		}
		if s != "Hello Jon." {
			t.Fatalf("not expected string")
		}
		fmt.Println("End TestPositive_01")
	})
	t.Run("正常系02 パラメーター(names []string)のサイズ2", func(t *testing.T) {
		beforeEach()      // テスト前処理
		defer afterEach() // テスト後処理

		s, err := Hello([]string{"Jon", "Mary"})
		if err != nil {
			t.Fatalf("err is not nil")
		}
		if s != "Hello Jon, Mary." {
			t.Fatalf("not expected string")
		}
		fmt.Println("End TestPositive_02")
	})
}

// 異常系テスト
func TestNegative(t *testing.T) {
	t.Run("異常系01 パラメーター(names []string)のサイズ0", func(t *testing.T) {
		beforeEach()      // テスト前処理
		defer afterEach() // テスト後処理

		s, err := Hello([]string{})
		if err == nil {
			t.Fatalf("err is nil")
		}
		if s != "" {
			t.Fatalf("not expected string")
		}
		fmt.Println("End TestNegative_01")
	})
}

実行結果

% go test -v
=== RUN   TestPositive
=== RUN   TestPositive/正常系01_パラメーター(names_[]string)のサイズ1
Start beforeEach()
End TestPositive_01
Start afterEach()
=== RUN   TestPositive/正常系02_パラメーター(names_[]string)のサイズ2
Start beforeEach()
End TestPositive_02
Start afterEach()
--- PASS: TestPositive (0.00s)
    --- PASS: TestPositive/正常系01_パラメーター(names_[]string)のサイズ1 (0.00s)
    --- PASS: TestPositive/正常系02_パラメーター(names_[]string)のサイズ2 (0.00s)
=== RUN   TestNegative
=== RUN   TestNegative/異常系01_パラメーター(names_[]string)のサイズ0
Start beforeEach()
End TestNegative_01
Start afterEach()
--- PASS: TestNegative (0.00s)
    --- PASS: TestNegative/異常系01_パラメーター(names_[]string)のサイズ0 (0.00s)
PASS
ok      example 0.177s

最後に

自分がGo言語でテストコードを記載するうえで、jestやJUnitで出来たあの書き方は、Go言語ではどのように実装するのか気になったので調べてまとめてみました。

これらのサンプルはいずれかのみ適用するのもよいですし、全て適用するのも可能です。
上手く使いこなして保守性に優れ、価値あるテストコードが作れるようになればと思います。

レスキューナウテックブログ

Discussion