testing

package
v0.64.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package testing provides utilities for testing OpenTelemetry instrumentation in GoBricks applications.

This package offers in-memory exporters and helpers for asserting spans and metrics in unit tests without requiring external collectors or backends.

Usage:

// Install a test trace provider globally; the helper restores the previous
// provider (and propagator) in t.Cleanup. Never Shutdown a provider you
// installed: otel's DEFAULT delegating provider binds to the first one
// installed in the binary and never rebinds (internal/global/state.go
// sync.Once), so shutting yours down can silence otel.Tracer calls made
// through that restored default provider afterwards. (While your provider
// is the current global, calls reach it directly — the hazard is what
// happens after you put the default back.)
tp := InstallTestTraceProvider(t)

// Run your code that creates spans
tracer := tp.TestTracer()
_, span := tracer.Start(context.Background(), "test-span")
span.End()

// Assert spans
spans := tp.Exporter.GetSpans()
require.Len(t, spans, 1)
AssertSpanName(t, spans[0], "test-span")

Index

Examples

Constants

View Source
const LeakCanary = "gobricks-span-leak-canary-9f13"

LeakCanary is the string an ADR-083 test plants inside an error message before driving a framework span sink, so one constant decides what every such test looks for. Neither the name nor the value is credential-shaped: this file ships in the module, so a `password=`-style literal would match the generic-password rule of every secret scanner run against a consumer that vendors go-bricks, and a `Secret`-prefixed identifier trips gosec G101 here.

View Source
const (

	// TestTracerName is the default tracer name used in observability tests,
	// avoiding a hardcoded "test" string literal at each call site.
	TestTracerName = "test"
)

Variables

This section is empty.

Functions

func AssertExceptionTypeOnly added in v0.61.0

func AssertExceptionTypeOnly(t TB, span *tracetest.SpanStub, wantType string)

AssertExceptionTypeOnly asserts that span carries exactly one exception event, that it names wantType under exception.type, and that it carries NOTHING else — exception.message above all (ADR-083).

func AssertMetricCount

func AssertMetricCount(t *testing.T, rm metricdata.ResourceMetrics, expected int)

AssertMetricCount asserts the total number of metrics collected.

func AssertMetricDescription

func AssertMetricDescription(t *testing.T, rm metricdata.ResourceMetrics, metricName, expectedDesc string)

AssertMetricDescription asserts the description of a metric.

func AssertMetricExists

func AssertMetricExists(t *testing.T, rm metricdata.ResourceMetrics, metricName string)

AssertMetricExists asserts that a metric with the given name exists.

func AssertMetricValue

func AssertMetricValue(t *testing.T, rm metricdata.ResourceMetrics, metricName string, expectedValue any)

AssertMetricValue finds a metric by name and asserts its value. For Sum and Gauge metrics (counters, up-down counters, gauges), it asserts the first data point value. For Histogram metrics, it asserts the count.

func AssertNoExceptionEvent added in v0.61.0

func AssertNoExceptionEvent(t TB, span *tracetest.SpanStub)

AssertNoExceptionEvent is the negative of AssertExceptionTypeOnly: a span that recorded no error must carry no exception event.

func AssertNoSpanMarkers added in v0.61.0

func AssertNoSpanMarkers(t TB, span *tracetest.SpanStub, markers ...string)

AssertNoSpanMarkers asserts that no marker reaches ANY span sink: the status description, the span name, the span attributes, the event names, or the event attributes — keys as well as values, since a key is exported too.

func AssertSpanAttribute

func AssertSpanAttribute(t *testing.T, span *tracetest.SpanStub, key string, expected any)

AssertSpanAttribute asserts that a span has a specific attribute with the expected value.

func AssertSpanError

func AssertSpanError(t *testing.T, span *tracetest.SpanStub, expectedDesc string)

AssertSpanError asserts that a span has an error status with the expected description.

func AssertSpanName

func AssertSpanName(t *testing.T, span *tracetest.SpanStub, expected string)

AssertSpanName asserts the name of a span.

func AssertSpanStatus

func AssertSpanStatus(t *testing.T, span *tracetest.SpanStub, expectedCode codes.Code)

AssertSpanStatus asserts the status of a span.

func AssertSpanStatusDescription

func AssertSpanStatusDescription(t *testing.T, span *tracetest.SpanStub, expectedDesc string)

AssertSpanStatusDescription asserts the status description of a span.

func FindMetric

func FindMetric(rm metricdata.ResourceMetrics, metricName string) *metricdata.Metrics

FindMetric finds a metric by name in the ResourceMetrics. Returns nil if not found.

func GetMetricHistogramCount

func GetMetricHistogramCount(rm metricdata.ResourceMetrics, metricName string) (uint64, error)

GetMetricHistogramCount gets the count for a Histogram metric. Returns error if metric not found or wrong type.

func GetMetricSumValue

func GetMetricSumValue(rm metricdata.ResourceMetrics, metricName string) (any, error)

GetMetricSumValue gets the sum value for a Sum[int64] or Sum[float64] metric. Returns error if metric not found or wrong type.

func HistogramExemplars added in v0.60.0

func HistogramExemplars[N int64 | float64](t TB, rm metricdata.ResourceMetrics, metricName string) []metricdata.Exemplar[N]

HistogramExemplars returns the exemplars attached to every data point of a histogram metric.

func SumExemplars added in v0.60.0

func SumExemplars[N int64 | float64](t TB, rm metricdata.ResourceMetrics, metricName string) []metricdata.Exemplar[N]

SumExemplars is HistogramExemplars for a counter.

Types

type SpanCollector

type SpanCollector struct {
	// contains filtered or unexported fields
}

SpanCollector provides a fluent API for filtering and asserting on captured spans.

Example

ExampleSpanCollector demonstrates filtering spans by name and attributes. Note: In actual tests, use NewSpanCollector with a testing.T parameter for assertions.

package main

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel/attribute"

	obtest "github.com/gaborage/go-bricks/observability/testing"
)

const (
	httpMethodAttr = "http.method"
	dbSystemAttr   = "db.system"

	databaseQuerySpan = "database-query"
)

func main() {
	tp := obtest.NewTestTraceProvider()
	defer tp.Shutdown(context.Background())

	tracer := tp.Tracer("example")

	// Create multiple spans with different attributes
	_, span1 := tracer.Start(context.Background(), databaseQuerySpan)
	span1.SetAttributes(attribute.String(dbSystemAttr, "postgresql"))
	span1.End()

	_, span2 := tracer.Start(context.Background(), "http-request")
	span2.SetAttributes(attribute.String(httpMethodAttr, "POST"))
	span2.End()

	_, span3 := tracer.Start(context.Background(), databaseQuerySpan)
	span3.SetAttributes(attribute.String(dbSystemAttr, "mysql"))
	span3.End()

	// Get spans and count them by filtering
	spans := tp.Exporter.GetSpans()

	// Count database query spans
	dbCount := 0
	pgCount := 0
	for i := range spans {
		if spans[i].Name == databaseQuerySpan {
			dbCount++
			// Check for PostgreSQL attribute
			for _, attr := range spans[i].Attributes {
				if attr.Key == attribute.Key(dbSystemAttr) && attr.Value.AsString() == "postgresql" {
					pgCount++
					break
				}
			}
		}
	}

	fmt.Printf("Database queries: %d\n", dbCount)
	fmt.Printf("PostgreSQL queries: %d\n", pgCount)

}
Output:
Database queries: 2
PostgreSQL queries: 1

func NewSpanCollector

func NewSpanCollector(t *testing.T, exporter *tracetest.InMemoryExporter) *SpanCollector

NewSpanCollector creates a span collector from an in-memory exporter.

func (*SpanCollector) AssertCount

func (sc *SpanCollector) AssertCount(expected int) *SpanCollector

AssertCount asserts the number of collected spans.

func (*SpanCollector) AssertEmpty

func (sc *SpanCollector) AssertEmpty() *SpanCollector

AssertEmpty asserts that the collection is empty.

func (*SpanCollector) First

func (sc *SpanCollector) First() tracetest.SpanStub

First returns the first span in the collection. Fails the test if the collection is empty.

func (*SpanCollector) Get

func (sc *SpanCollector) Get(index int) tracetest.SpanStub

Get returns the span at the given index. Fails the test if the index is out of bounds.

func (*SpanCollector) Len

func (sc *SpanCollector) Len() int

Len returns the number of collected spans.

func (*SpanCollector) WithAttribute

func (sc *SpanCollector) WithAttribute(key string, value any) *SpanCollector

WithAttribute filters spans by attribute key-value pair and returns a new collector.

func (*SpanCollector) WithName

func (sc *SpanCollector) WithName(name string) *SpanCollector

WithName filters spans by name and returns a new collector.

type TB added in v0.60.0

type TB interface {
	require.TestingT
	Helper()
}

TB is what the exemplar helpers need from *testing.T. It is an interface so a caller that already abstracts over the test handle can pass its own.

type TestMeterProvider

type TestMeterProvider struct {
	*sdkmetric.MeterProvider
	Reader *sdkmetric.ManualReader
}

TestMeterProvider wraps the SDK MeterProvider and manual reader for testing.

func InstallTestMeterProvider added in v0.64.0

func InstallTestMeterProvider(t *testing.T) *TestMeterProvider

InstallTestMeterProvider creates a test meter provider, installs it as the global meter provider, and registers a t.Cleanup that restores the previous provider. It leaves the text-map propagator alone.

Like InstallTestTraceProvider it never calls Shutdown: otel's default delegating provider binds to the FIRST provider installed in the binary via a sync.Once and never rebinds, so shutting an installed provider down would silently stop every later otel.Meter call that routes through that delegate.

The OTel globals are process-wide, so the install-to-cleanup window is not safe to overlap: do not call this from a t.Parallel test, and do not nest it inside another install whose cleanup runs later. Cleanups unwind LIFO, so sequential nesting restores correctly, but two overlapping installs can capture each other's provider and restore the wrong one.

Example:

mp := InstallTestMeterProvider(t)
counter, _ := otel.Meter("test").Int64Counter("test.counter")
counter.Add(context.Background(), 1)
rm := mp.Collect(t)

func NewTestMeterProvider

func NewTestMeterProvider() *TestMeterProvider

NewTestMeterProvider creates a MeterProvider with a manual reader for testing. The manual reader allows collecting metrics on-demand for assertions without periodic exports.

Example:

mp := NewTestMeterProvider()

// To install it globally, use InstallTestMeterProvider(t), which restores
// the previous provider in t.Cleanup; never Shutdown a provider you
// installed (see the package doc — otel's delegate binds once).
meter := mp.Meter("test")
counter, _ := meter.Int64Counter("test.counter")
counter.Add(context.Background(), 1)

// Collect metrics for assertions
rm := mp.Collect(t)
Example

ExampleNewTestMeterProvider demonstrates how to use the test meter provider to assert metrics created by your code.

package main

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel"

	obtest "github.com/gaborage/go-bricks/observability/testing"
)

func main() {
	// Create a test meter provider with manual reader
	mp := obtest.NewTestMeterProvider()

	// Set as global provider (or use directly), and restore the previous one when
	// done. Do NOT shut mp down — see ExampleNewTestTraceProvider (#1093).
	prev := otel.GetMeterProvider()
	defer otel.SetMeterProvider(prev)
	otel.SetMeterProvider(mp)

	// Your code that records metrics
	meter := mp.Meter("example-service")
	counter, _ := meter.Int64Counter("requests.count") // Metric options can be added here

	counter.Add(context.Background(), 1) // Attributes can be added to metric recordings

	counter.Add(context.Background(), 2)

	// Collect metrics for assertions
	// In real usage, you would pass the testing.T instance
	// rm := mp.Collect(t)
	fmt.Println("Metrics collected successfully")
}
Output:
Metrics collected successfully

func (*TestMeterProvider) Collect

Collect reads all metrics from the provider and returns them as ResourceMetrics. This is a convenience wrapper around Reader.Collect() with error handling.

type TestTraceProvider

type TestTraceProvider struct {
	*sdktrace.TracerProvider
	Exporter *tracetest.InMemoryExporter
}

TestTraceProvider wraps the SDK TracerProvider and in-memory exporter for testing.

func InstallTestTraceProvider added in v0.64.0

func InstallTestTraceProvider(t *testing.T) *TestTraceProvider

InstallTestTraceProvider creates a test trace provider, installs it as the global tracer provider together with a W3C trace-context propagator, and registers a t.Cleanup that restores the previous provider and propagator.

It never calls Shutdown, on the provider it created or any other: otel's default delegating provider binds to the FIRST provider installed in the binary via a sync.Once and never rebinds, so a later otel.Tracer call made through that restored default still routes into the first-installed provider. Shutting one down would make those calls record nothing, with no error. Restoring the previous provider is safe; shutting one down is not.

The OTel globals are process-wide, so the install-to-cleanup window is not safe to overlap: do not call this from a t.Parallel test, and do not nest it inside another install whose cleanup runs later. Cleanups unwind LIFO, so sequential nesting restores correctly, but two overlapping installs can capture each other's provider and restore the wrong one.

Example:

tp := InstallTestTraceProvider(t)
_, span := tp.TestTracer().Start(context.Background(), "operation")
span.End()
spans := tp.Exporter.GetSpans()

func NewTestTraceProvider

func NewTestTraceProvider() *TestTraceProvider

NewTestTraceProvider creates a TracerProvider with an in-memory exporter for testing. The returned provider captures all spans in memory for assertion without sending them to an external backend.

Example:

tp := NewTestTraceProvider()

// To install it globally, use InstallTestTraceProvider(t), which restores
// the previous provider in t.Cleanup; never Shutdown a provider you
// installed (see the package doc — otel's delegate binds once).

// Later, get spans for assertions
spans := tp.Exporter.GetSpans()
Example

ExampleNewTestTraceProvider demonstrates how to use the test trace provider to assert spans created by your code.

package main

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"

	obtest "github.com/gaborage/go-bricks/observability/testing"
)

const httpMethodAttr = "http.method"

func main() {
	// Create a test trace provider with in-memory exporter
	tp := obtest.NewTestTraceProvider()

	// Set as global provider (or use directly), and restore the previous one when
	// done. Do NOT shut tp down: otel's default delegating provider binds to the
	// first provider installed in the binary and never rebinds
	// (internal/global/state.go sync.Once, #1093), so shutting tp down can silence
	// otel.Tracer calls made through that restored default afterwards.
	prev := otel.GetTracerProvider()
	defer otel.SetTracerProvider(prev)
	otel.SetTracerProvider(tp)

	// Your code that creates spans
	tracer := tp.Tracer("example-service")
	_, span := tracer.Start(context.Background(), "process-request")
	span.SetAttributes(
		attribute.String(httpMethodAttr, "GET"),
		attribute.String("http.url", "/api/users"),
		attribute.Int("http.status_code", 200),
	)
	span.End()

	// Get captured spans for assertions
	spans := tp.Exporter.GetSpans()
	fmt.Printf("Captured %d spans\n", len(spans))
}
Output:
Captured 1 spans

func (*TestTraceProvider) TestTracer added in v0.18.0

func (ttp *TestTraceProvider) TestTracer() trace.Tracer

TestTracer returns a tracer with the standard test name. This is a convenience method that eliminates the need to pass TestTracerName to the Tracer() method in every test.

Example:

tp := NewTestTraceProvider()
tracer := tp.TestTracer()
_, span := tracer.Start(context.Background(), "operation")

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL