PR

Go言語(Golang)testing/synctestの使い方|Go1.25で並行処理・非同期処理テストを高速化

2. 基礎

こんにちは。Tomoyuki(@tomoyuki65)です。

Go言語(Golang)のバージョン1.25から並行処理・非同期処理テストを高速化するための「testing/synctest」が追加されました。

この記事では、そんなGo言語(Golang)のtesting/synctestの使い方についてご紹介します。

 

Go言語(Golang)testing/synctestの使い方|Go1.25で並行処理・非同期処理テストを高速化

testing/synctestとは?

「testing/synctest」は、並行処理・非同期処理のテストを決定論的かつ高速に書くための標準パッケージです。

特に次の問題を解決するために作られています。

・time.Sleep を使った不安定(flaky)なテスト
・goroutineの実行順に依存するテスト
・「5秒待つ」みたいな遅いテスト
・timeout/retry/backoffの検証

 

また、「synctest」の最大の特徴は以下の通りです。

・仮想時間(fake clock)
・goroutineの状態追跡
・bubble(隔離空間)

 

これにより、「synctest.Test()」の中では実時間ではなく「仮想時間」が進むため、例えば「time.Sleep(1 * time.Hour)」のようなコードがあっても実際には1時間待たず、goroutineが全部ブロック状態になるとsynctest が自動で時間を進めて処理します。

 

例えば以下のようなサンプルコードを例とします。

package main

import (
    "fmt"
    "time"
)

// ゴルーチンを使った関数
func asyncHello(ch chan<- string) {
    go func() {
        // 3秒待つ
        time.Sleep(3 * time.Second)
        // チャネルに文字列を送信
        ch <- "hello"
    }()
}

func main() {
    // チャネルを定義
    ch := make(chan string)

    // ゴルーチンを使った関数を実行
    asyncHello(ch)

    // チャネルから文字列を受信
    msg := <-ch

    // ログ出力
   fmt.Println(msg)
}

 

実行結果としては、3秒後に文字列「hello」が出力されます。

hello

 

この処理のゴルーチンを使った関数「asyncHello」に関するテストコードを書く際に、Go1.24以前では以下のように書いたりします。

package main

import (
    "testing"
    "time"

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

func TestOldAsyncHello(t *testing.T) {
    // チャネルを定義
    ch := make(chan string)

    // ゴルーチンを使った関数を実行
    asyncHello(ch)

    // 検証
    select {
    // チャネルを受信した場合
    case msg := <-ch:
        assert.Equal(t, "hello", msg)
    // タイムアウト設定
    case <-time.After(5 * time.Second):
        t.Fatal("timeout")
    }
}

 

このテストコードを実行すると、以下のようにテストの完了には3秒かかってしまいます。

 

次にこのテストコードをGo1.25以上で使える「testing/synctest」を使って修正すると以下のようになります。

package main

import (
    "testing"
    "testing/synctest"
    "time"

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

func TestAsyncHello(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        // チャネルを定義
        ch := make(chan string)

        // ゴルーチンを使った関数を実行
        asyncHello(ch)

        // 検証
        select {
        // チャネルを受信した場合
        case msg := <-ch:
            assert.Equal(t, "hello", msg)
        // タイムアウト設定
        case <-time.After(5 * time.Second):
            t.Fatal("timeout")
        }
    })
}

 

このテストコードを実行すると以下のようにすぐに完了するため、テストコードの高速化が可能です。

 

synctestが有効かの判断について

synctestは、Goの中だけで閉じた並行処理・非同期処理を決定論的に進められる仕組みとなっているため、全ての並行処理・非同期処理に有効なわけではありません。

そのため、synctestが有効かの判断については主に以下のようになります。

・「time / channel / sync / context」だけで完結するような処理

 → synctestが有効 

・「ネットワーク / DB / ファイル / OS / 外部プロセス」が絡むような処理

 → 「mock / stub / interface」化などが必要

 

スポンサーリンク

最後に

今回はGo言語(Golang)のtesting/synctestの使い方についてご紹介しました。

synctestが有効であればテストコードの高速化が図れるため、CI(Continuous Integration:継続的インテグレーション)の処理時間を短縮して効率化を図れる可能性があります。

Goのバージョン1.25以上を利用する際は、ぜひ参考にしてみて下さい。

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はSoftware Engineer(Web/Gopher)をしています!

Tomoyukiをフォローする
2. 基礎
スポンサーリンク
Tomoyukiをフォローする

コメント

タイトルとURLをコピーしました