Testing Strategies for Kratos Middleware Components and Applications

Kratos middleware can be validated through unit tests with mock handlers, chain composition tests, transport-level integration tests with in-process servers, and end-to-end tests using the full application bootstrap.

The go-kratos/kratos framework implements middleware as pure Go functions that wrap a handler, enabling straightforward testing at multiple isolation levels. This guide covers the recommended testing strategies for Kratos middleware components and applications, from isolated logic verification to full integration suites.

Understanding the Kratos Middleware Contract

At its core, Kratos middleware relies on two functional types defined in middleware/middleware.go:

type Handler    func(ctx context.Context, req any) (any, error)
type Middleware func(Handler) Handler

A Middleware receives a Handler and returns a new Handler that may execute logic before, after, or around the inner handler. The Chain helper composes multiple middlewares into a single pipeline. According to the implementation in middleware/middleware.go, Chain applies middlewares in reverse order so the first element in the slice executes first during request processing.

Unit Testing Kratos Middleware Components

Unit tests for individual middleware should isolate the component from transport details. The standard pattern involves creating a mock handler that records execution or returns deterministic responses, wrapping it with the middleware under test, and asserting on the side effects.

Testing Individual Middleware with Mock Handlers

A robust unit test follows this structure:

  1. Create a mock Handler that captures arguments or returns fixed values.
  2. Apply the middleware using the constructor (e.g., recovery.Recovery()).
  3. Invoke the resulting handler with a test context and request.
  4. Assert that the middleware transformed the context, error, or response as expected.

Example: Testing the Recovery Middleware

The recovery middleware in middleware/recovery/recovery.go catches panics and converts them to errors. The corresponding test in middleware/recovery/recovery_test.go demonstrates how to verify context propagation and error transformation:

func TestRecoveryMiddleware(t *testing.T) {
    // Panic-inducing handler
    next := func(context.Context, any) (any, error) {
        panic("simulated panic")
    }

    // Recovery with custom handler to verify latency recording
    _, err := Recovery(WithHandler(func(ctx context.Context, _, r any) error {
        if _, ok := ctx.Value(Latency{}).(float64); !ok {
            t.Errorf("latency not recorded in context")
        }
        return errors.InternalServer("RECOVERY", fmt.Sprintf("panic: %v", r))
    }))(next)(context.Background(), "request")

    if err == nil {
        t.Fatal("expected error from panic recovery")
    }
}

This test verifies that the middleware catches the panic, records latency in the context, and returns a properly formatted internal server error.

Example: Testing the Validate Middleware

The validate middleware in middleware/validate/validate.go checks if requests implement a Validator interface. The test in middleware/validate/validate_test.go uses a mock handler that returns the request unchanged and asserts that validation errors are converted to BadRequest responses:

func TestValidateMiddleware(t *testing.T) {
    next := func(ctx context.Context, req any) (any, error) {
        return req, nil
    }

    // Test with invalid request
    invalidReq := &MockValidator{valid: false}
    _, err := Validator()(next)(context.Background(), invalidReq)
    
    if !errors.Is(err, errors.BadRequest) {
        t.Fatalf("expected BadRequest error, got %v", err)
    }
}

Testing Middleware Chains

When multiple middlewares are composed, you should verify that they execute in the correct order and that context values propagate through the chain. The Chain helper in middleware/middleware.go applies middlewares in reverse order, so the first middleware in the slice is the outermost wrapper.

The test in middleware/middleware_test.go demonstrates this by creating three dummy middlewares that increment a shared counter and verifying that the final count reflects the execution order:

func TestMiddlewareChain(t *testing.T) {
    var counter int
    makeMiddleware := func(id int) middleware.Middleware {
        return func(next middleware.Handler) middleware.Handler {
            return func(ctx context.Context, req any) (any, error) {
                counter += id
                return next(ctx, req)
            }
        }
    }

    // Chain applies in reverse: m1(m2(m3(next)))
    chain := middleware.Chain(makeMiddleware(1), makeMiddleware(10), makeMiddleware(100))
    handler := chain(func(ctx context.Context, req any) (any, error) {
        return "done", nil
    })

    handler(context.Background(), nil)
    // Expected: 1 + 10 + 100 = 111
    if counter != 111 {
        t.Fatalf("expected counter 111, got %d", counter)
    }
}

Integration Testing with Transport Layers

To verify that middleware functions correctly within the HTTP or gRPC transport pipelines, use the transport-specific test utilities. Kratos transports expose a WithMiddleware option that injects middleware into the server pipeline.

HTTP Transport Testing

The HTTP transport tests in transport/http/client_test.go demonstrate how to verify middleware execution in an in-process server. You can spin up an HTTP server with middleware configured, register a service implementation, and call it through the client stub:

func TestHTTPMiddlewareIntegration(t *testing.T) {
    // Service implementation
    svc := &testService{}
    
    // Create server with middleware
    srv := http.NewServer(
        http.Address(":0"),
        http.Middleware(
            middleware.Chain(
                recovery.Recovery(),
                validate.Validator(),
            ),
        ),
    )
    
    // Register service and start
    pb.RegisterTestServiceServer(srv, svc)
    go srv.Start()
    defer srv.Stop()
    
    // Create client
    client := pb.NewTestServiceClient(
        http.NewClient(http.WithEndpoint(srv.Endpoint())),
    )
    
    // Execute request and verify middleware effects
    resp, err := client.TestMethod(context.Background(), &pb.TestRequest{})
    if err != nil {
        t.Fatalf("request failed: %v", err)
    }
    // Assert response...
}

gRPC Transport Testing

The same pattern applies to gRPC transports using transport/grpc. Configure middleware via the server options and use the generated gRPC client to verify behavior across the network boundary.

End-to-End Testing for Kratos Applications

For comprehensive validation, test the complete application bootstrap using kratos.New. This approach verifies that middleware is correctly wired into the application lifecycle, context propagation works across transport layers, and the internal matcher (internal/matcher/middleware.go) respects middleware ordering.

func TestAppEndToEnd(t *testing.T) {
    // Build complete application
    app := kratos.New(
        kratos.Name("test-app"),
        kratos.Metadata(map[string]string{"env": "test"}),
        kratos.Server(
            http.NewServer(
                http.Address(":0"),
                http.Middleware(
                    middleware.Chain(
                        recovery.Recovery(),
                        logging.Logging(logger),
                        validate.Validator(),
                    ),
                ),
            ),
        ),
    )
    
    // Start application in background
    go func() { _ = app.Run() }()
    defer app.Stop()
    
    // Allow server to start
    time.Sleep(100 * time.Millisecond)
    
    // Perform real HTTP calls
    client := http.NewClient(
        http.WithEndpoint(fmt.Sprintf("http://%s", app.Endpoint())),
    )
    
    // Test valid request
    resp, err := client.Invoke(context.Background(), "GET", "/test", nil)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    // Verify middleware effects (logging output, headers, etc.)
    // ...
}

This end-to-end approach confirms that middleware ordering is preserved by the framework's internal matcher and that context values propagate correctly through the entire request lifecycle.

Common Pitfalls and How to Avoid Them

When testing Kratos middleware, avoid these frequent mistakes:

Pitfall Why It Matters Solution
Missing context value assertions Middleware often stores latency, tracing IDs, or metadata in context that tests fail to verify. Always assert on context values after the handler returns, as demonstrated in the Recovery test.
Incorrect chain order expectations The Chain helper applies middlewares in reverse order; tests expecting forward execution will fail. Use the Chain implementation as the source of truth and verify side-effects match the reverse application order.
Real network dependencies in unit tests Using actual network calls slows CI and introduces flakiness. Stick to in-process transport tests or mock the transport layer entirely.
String-based error comparison Kratos error types carry metadata; string comparison ignores error codes and details. Use errors.Is() or errors.Code() for proper error validation.

Summary

Testing Kratos middleware components and applications requires a layered approach that leverages the framework's functional design:

  • Unit tests isolate individual middleware using mock handlers to verify logic, error transformation, and context manipulation.
  • Chain tests validate middleware ordering and interaction using the Chain helper with shared state assertions.
  • Integration tests verify transport-layer wiring by spinning up in-process HTTP or gRPC servers with middleware configured via WithMiddleware.
  • End-to-end tests bootstrap complete applications using kratos.New to ensure middleware behaves correctly across the full request lifecycle.

By combining these strategies and avoiding common pitfalls like incorrect chain order assumptions or missing context assertions, you can ensure your Kratos middleware performs reliably in production environments.

Frequently Asked Questions

How do I test middleware that modifies context values?

To test context modification, create a mock handler that retrieves values from the context after the middleware executes. In your test, invoke the wrapped handler and then assert that the context contains the expected keys and values. For example, when testing the Recovery middleware, verify that the Latency value is stored in the context by accessing it within a custom recovery handler or by checking context values passed to subsequent handlers.

What is the correct order for testing middleware chains?

When testing chains, remember that the Chain helper in middleware/middleware.go applies middlewares in reverse order, meaning the first middleware in the slice becomes the outermost wrapper. To verify correct ordering, create middlewares that increment a shared counter or append identifiers to a slice, then assert that the side effects occur in the reverse order of the slice index. This ensures your tests align with the actual execution flow implemented by the framework.

How do I perform integration testing without real network calls?

Use the in-process server capabilities provided by the HTTP and gRPC transports. Create a server using http.NewServer or grpc.NewServer with http.Address(":0") (which assigns a random available port), register your service, and start the server in a goroutine. Then create a client using the server's actual endpoint (srv.Endpoint()) to perform real RPC calls through the middleware stack without external network dependencies or port conflicts.

How do I verify panic recovery in middleware tests?

To test panic recovery, create a mock handler that deliberately calls panic() with a specific message. Wrap this handler with the Recovery middleware configured with a custom handler function that captures the panic reason and context values. Invoke the wrapped handler and assert that the returned error contains the expected recovery message and that no panic escapes the test. Verify that context values like latency are properly recorded even during panic scenarios by checking them within the custom recovery handler.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →