📝

OpenTelemetryのspanをテストに使う

2024/02/15に公開

こんにちは。しろうです。

OpenTelemetry(以下oTel)でspanにいろいろな情報を詰めているのでその情報を go test でテストを実行するときに使えないかなぁと思っていました。その方法を見つけたのでここに記録しておきます。

実行コード

var tracer = otel.Tracer("github.com/shirou/sample/test")

みたいな感じでpackageレベルでtracerを設定してあるとします。

そして、実行コードとして以下のような関数があるとします。ほんとにこんな関数があったらとても驚きますが。

func Foo(ctx context.Context, value int) error {
	ctx, span := tracer.Start(ctx, "Foo")
	defer span.End()

	switch value{
		case 1:
			foo1(ctx)
		case 2:
			foo2(ctx)
		default:
			span.SetStatus(codes.Error, "unknown value")
			return fmt.Errorf("unknown value specified, %d", value)
	}
	return nil
}

func foo1(ctx context.Context) error {
	ctx, span := tracer.Start(ctx, "foo1")
	defer span.End()
	return nil
}

func foo2(ctx context.Context) error {
	ctx, span := tracer.Start(ctx, "foo2")
	defer span.End()
	return nil
}

テストコード

さて、このFooをテストしたいですね。そして、ちゃんとfoo1あるいはfoo2が呼ばれていることをテストの中で確認したくなると思います。このときに、せっかくつけているspanを使いたくなるはずです。え、「プライベートメソッドはテストするべきではない」ですって?まあそういう人もいるかと思います。

それはともかく、こういうときは tracetest の中の InMemoryExporter を使うとうまくspanを取り出せます。

import (
	"go.opentelemetry.io/otel/sdk/trace/tracetest"
)

func TestServerFoo(t *testing.T) {
	// InMemoryExporterを作成
	spanChecker := tracetest.NewInMemoryExporter()
	tracerProvider := sdktrace.NewTracerProvider(
		sdktrace.WithSyncer(spanChecker),  // WithSyncerはテスト用のexporter設定
	)
	// tracerを置き換える
	tracer = tracerProvider.Tracer("test")

	Foo(context.Background(), 1)
	spanInvoked(t, spanChecker.GetSpans(), "foo1")  // 下に記載してあるユーティリティ関数

	spanChecker.Reset() // 注意: Reset()しないと積み重なってしまう
	Foo(context.Background(), 2)
	spanInvoked(t, spanChecker.GetSpans(), "foo2")
}

こうしておくと、spanChecker.GetSpans() で呼び出されたspanが全て取り出せます。あとは以下のようなユーティリティ関数を用意しておけばいいでしょう。

func spanInvoked(t *testing.T, spans tracetest.SpanStubs, name string) {
	t.Helper()
	for _, span := range spans {
		if span.Name == name {
			return
		}
	}
	t.Errorf("%s should be invoked", name)
}

あるいは、StatusにErrorがセットされているかを確認したりしてもいいですね。

func spanError(t *testing.T, spans tracetest.SpanStubs) {
	t.Helper()
	for _, span := range spans {
		if span.Status.Code == codes.Error {
			return
		}
	}
	t.Errorf("should has an error")
}

その他 tracetest.SpanStub にはattributeやEventも記録されているのでそれらを確認するテストを書くのも良いかと思います。

注意点

  • InMemoryExporter.Reset() を呼ばないとspanがどんどん積み重なってしまいます
  • 上記例では平気ですが、InMemoryExporterをテスト間で使い回す場合、parallelでは意図通りには動かない可能性があります

まとめ

  • tracetest 便利

そんなこんなで。

Discussion