🧪

【Go】Goのテストに入門してみた! ~並行テスト編~

に公開

はじめに

前回は、「テストの前処理・後処理」を見ていきました。
今回は、「並行テスト」に入門します!

前回の記事はこちら!
https://zenn.dev/tmyhrn/articles/a93e750fde5d28

この記事でわかること

  • テストを並行実行する方法
  • テスト結果の見方
  • 並行実行するケース

普通にテストしてみる

擬似的に、以下のような高負荷な処理を実行します。

main_test.go
package main

import (
    "testing"
    "time"
    "unicode/utf8"
)

func Length(s string) int {
    // 擬似的に高負荷にする
    time.Sleep(5 * time.Second)
    return utf8.RuneCountInString(s)
}

func TestLength(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  int
    }{
        {"ascii", "Hello", 5},
        {"full-width-japanese", "こんにちは", 5},
        {"half-width-japanese", "コンニチハ", 5},
        {"emoji", "😃😃😃", 3},
        {"empty", "", 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Length(tt.input); got != tt.want {
                t.Errorf("Length = %d, want %d", got, tt.want)
            }
        })
    }
}

実行すると、以下のような結果になります。

ターミナル
# go test -v

=== RUN   TestLength
=== RUN   TestLength/ascii
=== RUN   TestLength/full-width-japanese
=== RUN   TestLength/half-width-japanese
=== RUN   TestLength/emoji
=== RUN   TestLength/empty
--- PASS: TestLength (25.01s)
    --- PASS: TestLength/ascii (5.00s)
    --- PASS: TestLength/full-width-japanese (5.00s)
    --- PASS: TestLength/half-width-japanese (5.00s)
    --- PASS: TestLength/emoji (5.00s)
    --- PASS: TestLength/empty (5.00s)
PASS
ok      go-test-practice        25.360s

各処理で5秒スリープしているので、5つのサブテストを実行し、合計で25秒かかっています。

並列テストをしてみる

次は、並列でテストを実行してみます。

main_test.go
package main

import (
    "testing"
    "time"
    "unicode/utf8"
)

func Length(s string) int {
    // 擬似的に高負荷にする
    time.Sleep(5 * time.Second)
    return utf8.RuneCountInString(s)
}

func TestLength(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  int
    }{
        {"ascii", "Hello", 5},
        {"full-width-japanese", "こんにちは", 5},
        {"half-width-japanese", "コンニチハ", 5},
        {"emoji", "😃😃😃", 3},
        {"empty", "", 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            if got := Length(tt.input); got != tt.want {
                t.Errorf("Length = %d, want %d", got, tt.want)
            }
        })
    }
}

実行すると、以下のような結果になります。

ターミナル
# go test -v

=== RUN   TestLength
=== RUN   TestLength/ascii
=== PAUSE TestLength/ascii
=== RUN   TestLength/full-width-japanese
=== PAUSE TestLength/full-width-japanese
=== RUN   TestLength/half-width-japanese
=== PAUSE TestLength/half-width-japanese
=== RUN   TestLength/emoji
=== PAUSE TestLength/emoji
=== RUN   TestLength/empty
=== PAUSE TestLength/empty
=== CONT  TestLength/ascii
=== CONT  TestLength/emoji
=== CONT  TestLength/half-width-japanese
=== CONT  TestLength/full-width-japanese
=== CONT  TestLength/empty
--- PASS: TestLength (0.00s)
    --- PASS: TestLength/emoji (5.00s)
    --- PASS: TestLength/empty (5.00s)
    --- PASS: TestLength/full-width-japanese (5.00s)
    --- PASS: TestLength/ascii (5.00s)
    --- PASS: TestLength/half-width-japanese (5.00s)
PASS
ok      go-test-practice        5.353s

並行テスト前は、5つのサブテストを順次実行したことで、合計で25秒かかっていました。
しかし、テストを並行実行したことで、約5秒でテストが終了しました。

並行テスト結果の見方

1. RUNとPAUSE

「RUN」でサブテストが開始されますが、t.Parallel() が呼ばれると一時停止状態に「PAUSE」になります。

2. CONT

全てのサブテストが一時停止になり、実行可能状態になったところで、一斉にサブテストを実行し「CONT(Continue)」になります。

並行テストを実行するケース

1. 大量のテストケースがある場合

逐次実行すると時間がかかるテストも、並行で処理すれば実行時間を短縮することができます。
ただし、テスト同士の独立性が担保されていたり、リソースの競合が起きないことに注意しましょう。

2. 外部依存がある場合

外部APIやサービス、データベースアクセスなどを利用する場合のテストも、並行で処理すれば実行時間を短縮することができます。
しかしこれも、テスト同士の独立性が担保されていたり、リソースの競合が起きないことに注意しましょう。

まとめ

今回はテストを並行実行する方法を学びました。
t.Parallel() を使ってテストを並行実行することができます。
処理が重いテストや大量のテストケースを効率的にこなすには、テストを並行実行するのが有効です。
ただし、共有リソースや状態に依存するテストを並列実行すると競合が発生する恐れがあるため、テストの独立性を保つことが重要です。

次回は、「カバレッジ」に入門します!

参考

https://future-architect.github.io/articles/20250509a/#テストを並列に実施したい
https://cool-ryo.hatenablog.com/entry/2024/04/30/180756

Discussion