Testing¶
MinimalLambda.Testing works like ASP.NET Core’s WebApplicationFactory: it boots your real Lambda
entry point in memory, speaking the same Runtime API contract that AWS uses. It is the
end-to-end/integration layer above pure unit tests: you run the real pipeline (handlers, middleware,
lifecycle hooks, DI) without deploying or opening ports.
When to Use¶
- End-to-end pipeline coverage – Exercise source-generated handlers, middleware, envelopes, and lifecycle hooks with real DI and serialization.
- Regression nets – Verify bootstrapping, cold-start logic, and error payloads stay stable.
- Host customization – Override configuration/services per test via
WithHostBuilder. - Prefer plain unit tests for isolated logic; reach for MinimalLambda.Testing when you need confidence in the Lambda runtime behavior.
Quick Start¶
Install both packages:
Package Versions
Ensure MinimalLambda.Testing version matches your MinimalLambda version.
Mismatched versions may cause runtime errors or unexpected behavior.
Write an end-to-end test with xUnit v3:
Invocation APIs¶
InvokeAsync<TEvent, TResponse>(event, token)– Send a strongly typed event, expect a typed response; fails with anInvocationResponse<TResponse>containing error details on handler exceptions.InvokeNoEventAsync<TResponse>(token)– Invoke a handler that does not take an event payload.InvokeNoResponseAsync<TEvent>(event, token)– Fire-and-forget style; skips response deserialization for handlers that returnvoid/Taskor write directly to streams.
InvocationResponse/InvocationResponse<T> include WasSuccess, Response, and structured
Error information that mirrors Lambda runtime error payloads—assert on these to verify failures.
Trace IDs¶
Pass a custom traceId to control the Lambda-Runtime-Trace-Id header for correlation in logs and telemetry:
If omitted, a new GUID is generated for each invocation.
Working with Cancellation¶
- Propagate test cancellation – Call
WithCancellationToken(...)on the factory to flow your test framework's token into the in-memory runtime. All server operations observe it. - Per-call tokens – Pass tokens to
StartAsyncandInvoke*to bound individual operations. - Pre-canceled tokens – A pre-canceled token will fail the invocation immediately (see
SimpleLambda_WithPreCanceledToken_CancelsInvocationin the test suite). - Automatic timeouts – Every invocation automatically times out after
LambdaServerOptions.FunctionTimeout(defaults to 3 seconds, matching AWS Lambda's default). The test server creates a linked cancellation token for each invocation that enforces this deadline, mirroring Lambda's actual timeout behavior. Adjustfactory.ServerOptions.FunctionTimeoutbefore invoking to test different timeout scenarios or catch slow handlers.
Host Customization and Fixtures¶
Override Host Configuration¶
Use WithHostBuilder to tweak services and configuration for a specific test run:
You can also override app configuration (ConfigureAppConfiguration) or swap the DI container using
UseServiceProviderFactory / ConfigureContainer (Autofac, etc.)—the factory will replay those
changes before the Lambda host boots.
Content Roots for File Fixtures¶
Add LambdaApplicationFactoryContentRootAttribute to your test assembly when you need a predictable
content root (e.g., static files, JSON fixtures). The factory will pick it up and set the content
root before booting the host.
Shared Fixtures with IClassFixture¶
Use xUnit's IClassFixture to share a single LambdaApplicationFactory across all tests in a class.
This improves test performance by reusing the same Lambda host instance:
Important: The same factory instance is used for all tests in the class. This means:
- OnInit runs once when the first test executes
- OnShutdown runs once when all tests complete
- Singleton services are shared across all tests in the class
- Do not use this pattern if you need to test initialization/shutdown behavior (use a fresh factory per test instead)
Custom Factory for Reusable Configuration¶
For more complex scenarios, extend LambdaApplicationFactory<TProgram> to create a reusable fixture
with pre-configured test doubles and settings:
This pattern is useful when:
- Multiple test classes need the same test setup
- You want to expose test doubles as properties for easy configuration
- You need consistent environment settings across many tests
Tuning the Runtime Shim¶
LambdaServerOptions controls the simulated runtime headers, timing, and serialization behavior.
Access it via factory.ServerOptions before starting the server if you need test-specific values:
- Runtime headers –
FunctionArn,AdditionalHeadersfor custom Lambda runtime headers - Timeout behavior –
FunctionTimeoutcontrols invocation deadline (defaults to 3 seconds) - JSON serialization –
SerializerOptionscontrols how the test server serializes events and responses sent to your handler
Initialization and Shutdown Behavior¶
StartAsyncreturnsInitResponsewithInitStatusvalues:InitCompleted/InitAlreadyCompleted– Ready to invoke.InitError– AnErrorResponsefrom OnInit failures; server stops itself.HostExited– Entry point exited early (e.g., OnInit signaled stop).Invoke*will start the server on-demand; if init fails it throws with the reported status.StopAsynctriggers OnShutdown and aggregates any exceptions (surfaced asAggregateException).DisposeAsyncis idempotent; safe to call multiple times.
StartAsync is Optional
InvokeAsync will automatically call StartAsync if you haven't called it explicitly.
When to call StartAsync explicitly:
- To inspect
InitStatusbefore invoking - To measure cold start time separately
- To ensure OnInit completes before tests run
When to skip StartAsync:
- Simple handler tests where init success is assumed
- Tests focused on invocation behavior, not initialization
Testing Patterns¶
Testing Error Responses¶
Validate error handling by asserting on InvocationResponse.Error:
Testing Concurrent Invocations¶
The test server handles concurrent invocations safely with FIFO ordering:
Testing Middleware¶
Verify middleware behavior by inspecting response metadata or side effects:
Testing Lifecycle Hooks¶
OnInit That Signals Shutdown¶
OnInit That Throws Exceptions¶
OnShutdown Exception Handling¶
Alternative DI Containers¶
Replace the default DI container with Autofac, DryIoc, or other containers:
Performance and Cold Start Testing¶
Measure initialization and invocation performance:
Best Practices¶
- Reuse factories per class – Creating a new factory per test is fine; reuse within a class to speed up suites that share the same host configuration.
- Runtime headers – Responses include the same headers Lambda sends (
Lambda-Runtime-*plus anyAdditionalHeadersyou set); assert on them if you need to prove deadline/ARN behavior. - Fresh factory per test for lifecycle testing – When testing OnInit/OnShutdown, create a new factory per test so lifecycle hooks run predictably.
Fixture Reuse Pitfalls
- Using
IClassFixture/ICollectionFixturewith a singleLambdaApplicationFactorymeans one host instance is shared across all tests in that scope. Avoid this pattern if you need to test startup/shutdown logic—use a fresh factory per test so OnInit/OnShutdown run predictably. - Do not mix a fixture-based factory with new factories created inside individual tests; they can
overlap and run simultaneously, leading to multiple hosts executing in parallel and surprising
side effects. Choose one approach (per-test or shared fixture) for a given test class/collection
and clean up via
DisposeAsync/StopAsyncwhen done.
Complete Examples¶
For comprehensive examples covering all scenarios, see the MinimalLambda.Testing test suite:
SimpleLambdaTests.cs– Basic invocation patterns and concurrent invocationsDiLambdaTests.cs– DI container replacement and lifecycle testingNoEventLambdaTests.cs– Configuration overrides and handlers without eventsNoResponseLambdaTests.cs– Fire-and-forget handlers
The MinimalLambda.Testing source (src/MinimalLambda.Testing/) also contains additional examples of
host overrides, cancellation, and error handling patterns.