From 4d591ed6ce6055cfde0d46124ae59e13f6f285d4 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sat, 13 Dec 2025 22:44:49 -0500 Subject: [PATCH 1/6] chore(docs): add MinimalLambda.Testing package to README files - Updated references in multiple README files to include MinimalLambda.Testing. - Added badges for MinimalLambda.Testing with links to NuGet package and download stats. --- README.md | 1 + src/MinimalLambda.Abstractions/README.md | 1 + src/MinimalLambda.OpenTelemetry/README.md | 1 + src/MinimalLambda.Testing/README.md | 1 + src/MinimalLambda/README.md | 1 + 5 files changed, 5 insertions(+) diff --git a/README.md b/README.md index d9d8c5a4..78f27329 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ for building AWS Lambda functions. | [**MinimalLambda**](./src/MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](./src/MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](./src/MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Testing**](./src/MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | ### Envelopes Packages diff --git a/src/MinimalLambda.Abstractions/README.md b/src/MinimalLambda.Abstractions/README.md index 808ad6fc..326a587d 100644 --- a/src/MinimalLambda.Abstractions/README.md +++ b/src/MinimalLambda.Abstractions/README.md @@ -213,6 +213,7 @@ source handling. | [**MinimalLambda**](../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Testing**](../MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | | [**MinimalLambda.Envelopes.Sqs**](../Envelopes/MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../Envelopes/MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/MinimalLambda.OpenTelemetry/README.md b/src/MinimalLambda.OpenTelemetry/README.md index c5b17082..75028971 100644 --- a/src/MinimalLambda.OpenTelemetry/README.md +++ b/src/MinimalLambda.OpenTelemetry/README.md @@ -251,6 +251,7 @@ source handling. | [**MinimalLambda**](../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Testing**](../MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | | [**MinimalLambda.Envelopes.Sqs**](../Envelopes/MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../Envelopes/MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/MinimalLambda.Testing/README.md b/src/MinimalLambda.Testing/README.md index dc0a1231..9c9f299a 100644 --- a/src/MinimalLambda.Testing/README.md +++ b/src/MinimalLambda.Testing/README.md @@ -104,6 +104,7 @@ source handling. | [**MinimalLambda**](../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Testing**](../MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | | [**MinimalLambda.Envelopes.Sqs**](../Envelopes/MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../Envelopes/MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | diff --git a/src/MinimalLambda/README.md b/src/MinimalLambda/README.md index bd4a557e..85e20c5f 100644 --- a/src/MinimalLambda/README.md +++ b/src/MinimalLambda/README.md @@ -277,6 +277,7 @@ source handling. | [**MinimalLambda**](../MinimalLambda/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.svg)](https://www.nuget.org/packages/MinimalLambda/) | | [**MinimalLambda.Abstractions**](../MinimalLambda.Abstractions/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Abstractions.svg)](https://www.nuget.org/packages/MinimalLambda.Abstractions/) | | [**MinimalLambda.OpenTelemetry**](../MinimalLambda.OpenTelemetry/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.OpenTelemetry.svg)](https://www.nuget.org/packages/MinimalLambda.OpenTelemetry/) | +| [**MinimalLambda.Testing**](../MinimalLambda.Testing/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Testing.svg)](https://www.nuget.org/packages/MinimalLambda.Testing/) | | [**MinimalLambda.Envelopes.Sqs**](../Envelopes/MinimalLambda.Envelopes.Sqs/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sqs.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sqs/) | | [**MinimalLambda.Envelopes.ApiGateway**](../Envelopes/MinimalLambda.Envelopes.ApiGateway/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.ApiGateway.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.ApiGateway/) | | [**MinimalLambda.Envelopes.Sns**](../Envelopes/MinimalLambda.Envelopes.Sns/README.md) | [![NuGet](https://img.shields.io/nuget/v/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns) | [![Downloads](https://img.shields.io/nuget/dt/MinimalLambda.Envelopes.Sns.svg)](https://www.nuget.org/packages/MinimalLambda.Envelopes.Sns/) | From 50c342214018592aa6044f9ae200944d8aab3cc7 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 14 Dec 2025 12:55:23 -0500 Subject: [PATCH 2/6] docs(guides): document MinimalLambda.Testing usage and examples - Expanded testing guide with in-depth MinimalLambda.Testing documentation. - Added quick start, invocation API details, and host customization patterns. - Updated `index.md` with reference to MinimalLambda.Testing for in-memory tests. - Linked `README.md` to the testing guide for detailed examples. --- README.md | 3 +- docs/guides/index.md | 1 + docs/guides/testing.md | 145 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 139 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 78f27329..666eab07 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,8 @@ public class GreetingService : IGreetingService ``` See the [examples directory](./examples/) for more complete examples, including middleware and -OpenTelemetry integration. +OpenTelemetry integration. For in-memory integration tests, use +[MinimalLambda.Testing](./docs/guides/testing.md) (a `WebApplicationFactory`-style runtime shim). ## Documentation diff --git a/docs/guides/index.md b/docs/guides/index.md index 2ab18fc1..bb16e9fa 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -101,6 +101,7 @@ Write comprehensive tests for your Lambda functions using xUnit, NSubstitute, an - Testing handlers and middleware - Integration testing - Test naming conventions + - In-memory end-to-end testing with `MinimalLambda.Testing` (`WebApplicationFactory`-style runtime shim) ## Learning Path diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 6d157220..c908c457 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -1,13 +1,140 @@ # Testing -!!! info "Coming Soon" - This guide is currently under development. Check back soon for comprehensive testing documentation covering: +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. - - Unit testing Lambda handlers - - Testing with xUnit, NSubstitute, and AutoFixture - - Testing middleware components - - Testing lifecycle handlers (OnInit, OnShutdown) - - Integration testing strategies - - Mocking AWS Lambda context - - Best practices and patterns +## 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: + +```bash +dotnet add package MinimalLambda +dotnet add package MinimalLambda.Testing +``` + +Write an end-to-end test with xUnit v3: + +```csharp title="HelloWorldTests.cs" linenums="1" +using MinimalLambda.Testing; +using Xunit; + +public class HelloWorldTests +{ + [Fact] + public async Task HelloWorld_ReturnsGreeting() + { + await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(TestContext.Current.CancellationToken); + + // Optional: StartAsync mirrors Lambda init; InvokeAsync will start on demand if you skip this. + await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); + + var response = await factory.TestServer.InvokeAsync( + "World", + TestContext.Current.CancellationToken + ); + + Assert.True(response.WasSuccess); + Assert.Equal("Hello World!", response.Response); + } +} +``` + +## Invocation APIs + +- `InvokeAsync(event, token)` – Send a strongly typed event, expect a typed + response; fails with an `InvocationResponse` containing error details on handler + exceptions. +- `InvokeNoEventAsync(token)` – Invoke a handler that does not take an event payload. +- `InvokeNoResponseAsync(event, token)` – Fire-and-forget style; skips response + deserialization for handlers that return `void`/`Task` or write directly to streams. +- **Trace IDs** – Pass `traceId` into `InvokeAsync` to control the `Lambda-Runtime-Trace-Id` header + (defaults to a new GUID). + +`InvocationResponse`/`InvocationResponse` include `WasSuccess`, `Response`, and structured +`Error` information that mirrors Lambda runtime error payloads—assert on these to verify failures. + +## 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 `StartAsync` and `Invoke*` to bound individual operations. +- **Pre-canceled tokens** – A pre-canceled token will fail the invocation immediately (see + `SimpleLambda_WithPreCanceledToken_CancelsInvocation` in the test suite). +- **Timeouts** – Combine short tokens with `LambdaServerOptions.FunctionTimeout` to mirror Lambda’s + deadline behavior and catch slow handlers. + +## Host Customization and Fixtures + +### Override Host Configuration + +Use `WithHostBuilder` to tweak services and configuration for a specific test run: + +```csharp title="DI override" linenums="1" +await using var factory = new LambdaApplicationFactory() + .WithHostBuilder(builder => + { + builder.ConfigureServices((_, services) => + { + // Swap implementations or inject test doubles + services.Configure(options => + { + options.BootstrapOptions.RuntimeApiEndpoint = "http://localhost:9001"; + }); + }); + }); +``` + +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. + +### Tuning the Runtime Shim + +`LambdaServerOptions` controls the simulated runtime headers and timing (ARN, deadline/timeout, +extra headers). Access it via `factory.ServerOptions` before starting the server if you need +test-specific values. + +## Initialization and Shutdown Behavior + +- `StartAsync` returns `InitResponse` with `InitStatus` values: + - `InitCompleted` / `InitAlreadyCompleted` – Ready to invoke. + - `InitError` – An `ErrorResponse` from 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. +- `StopAsync` triggers OnShutdown and aggregates any exceptions (surfaced as `AggregateException`). +- `DisposeAsync` is idempotent; safe to call multiple times. + +## Patterns and Troubleshooting + +- **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. +- **Parallel invocations** – The test server is concurrency-safe; the concurrent invocation test in + the suite shows FIFO ordering. +- **Assert via DI** – Resolve services from `factory.TestServer.Services` to inspect metrics, + in-memory stores, or other state after invocations. +- **Error assertions** – Check `InvocationResponse.Error` for message and stack trace data; it + mirrors what the Lambda Runtime API returns. +- **Runtime headers** – Responses include the same headers Lambda sends (`Lambda-Runtime-*` plus any + `AdditionalHeaders` you set); assert on them if you need to prove deadline/ARN behavior. + +Ready to go deeper? The MinimalLambda.Testing source (`src/MinimalLambda.Testing/`) and its unit +tests (`tests/MinimalLambda.Testing.UnitTests/`) contain more examples of host overrides, +cancellation, and error handling patterns. From 84cc7d7e399204ff812acfbfa71ac6f3d69d55ed Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 14 Dec 2025 13:00:19 -0500 Subject: [PATCH 3/6] docs(getting-started): add link to MinimalLambda.Testing in testing section - Updated `index.md` to include reference to MinimalLambda.Testing for integration testing. --- docs/getting-started/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 8bc70f7b..ec32ab13 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -28,6 +28,8 @@ Before you begin, ensure you have: - **[Installation](installation.md)** – Add the NuGet packages and configure your csproj. - **[Your First Lambda](first-lambda.md)** – Walk through a handler, DI setup, and local testing. - **[Core Concepts](core-concepts.md)** – Learn about the host lifecycle, middleware, and source generation. +- **Testing** – For in-memory, `WebApplicationFactory`-style integration tests, see + [MinimalLambda.Testing](../guides/testing.md). Prefer to explore? Head directly to **[Guides](../guides/index.md)** or **[Examples](../examples/index.md)** for deeper dives. From 165b79afee516c5f1bea79765e2c2ea162da85ce Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 14 Dec 2025 13:02:46 -0500 Subject: [PATCH 4/6] docs(guides): explain fixture reuse pitfalls in testing guide - Added warning section to highlight issues with shared LambdaApplicationFactory. - Provided best practices for avoiding overlapping factory instances and side effects. - Emphasized using consistent patterns for test class/collection and proper cleanup. --- docs/guides/testing.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/guides/testing.md b/docs/guides/testing.md index c908c457..e26ec6d5 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -135,6 +135,15 @@ test-specific values. - **Runtime headers** – Responses include the same headers Lambda sends (`Lambda-Runtime-*` plus any `AdditionalHeaders` you set); assert on them if you need to prove deadline/ARN behavior. +!!! warning "Fixture reuse pitfalls" + - Using `IClassFixture`/`ICollectionFixture` with a single `LambdaApplicationFactory` means 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`/`StopAsync` when done. + Ready to go deeper? The MinimalLambda.Testing source (`src/MinimalLambda.Testing/`) and its unit tests (`tests/MinimalLambda.Testing.UnitTests/`) contain more examples of host overrides, cancellation, and error handling patterns. From 785528582b44898f07e0e9999ccabdf7327949bd Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 14 Dec 2025 13:32:02 -0500 Subject: [PATCH 5/6] docs(guides): enrich testing guide with advanced MinimalLambda.Testing use cases - Added detailed examples for error handling, middleware, and concurrent invocation testing. - Documented lifecycle hook testing patterns such as OnInit and OnShutdown scenarios. - Introduced performance testing guidance, including cold start and timeout testing. - Clarified usage of custom DI containers and JSON serialization options. - Included reference to the full MinimalLambda.Testing suite for comprehensive examples. --- docs/guides/testing.md | 333 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 316 insertions(+), 17 deletions(-) diff --git a/docs/guides/testing.md b/docs/guides/testing.md index e26ec6d5..afe88857 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -23,6 +23,10 @@ dotnet add package MinimalLambda dotnet add package MinimalLambda.Testing ``` +!!! warning "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: ```csharp title="HelloWorldTests.cs" linenums="1" @@ -38,7 +42,8 @@ public class HelloWorldTests .WithCancellationToken(TestContext.Current.CancellationToken); // Optional: StartAsync mirrors Lambda init; InvokeAsync will start on demand if you skip this. - await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); + var initResult = await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); + Assert.Equal(InitStatus.InitCompleted, initResult.InitStatus); var response = await factory.TestServer.InvokeAsync( "World", @@ -59,21 +64,38 @@ public class HelloWorldTests - `InvokeNoEventAsync(token)` – Invoke a handler that does not take an event payload. - `InvokeNoResponseAsync(event, token)` – Fire-and-forget style; skips response deserialization for handlers that return `void`/`Task` or write directly to streams. -- **Trace IDs** – Pass `traceId` into `InvokeAsync` to control the `Lambda-Runtime-Trace-Id` header - (defaults to a new GUID). `InvocationResponse`/`InvocationResponse` 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: + +```csharp +var response = await factory.TestServer.InvokeAsync( + new Request(), + TestContext.Current.CancellationToken, + traceId: "custom-trace-id-12345" +); + +// Verify trace ID was used in logging/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. + test framework's token into the in-memory runtime. All server operations observe it. - **Per-call tokens** – Pass tokens to `StartAsync` and `Invoke*` to bound individual operations. - **Pre-canceled tokens** – A pre-canceled token will fail the invocation immediately (see `SimpleLambda_WithPreCanceledToken_CancelsInvocation` in the test suite). -- **Timeouts** – Combine short tokens with `LambdaServerOptions.FunctionTimeout` to mirror Lambda’s - deadline behavior and catch slow handlers. +- **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. Adjust `factory.ServerOptions.FunctionTimeout` before + invoking to test different timeout scenarios or catch slow handlers. ## Host Customization and Fixtures @@ -122,20 +144,289 @@ test-specific values. - `StopAsync` triggers OnShutdown and aggregates any exceptions (surfaced as `AggregateException`). - `DisposeAsync` is idempotent; safe to call multiple times. -## Patterns and Troubleshooting +!!! tip "StartAsync is Optional" + `InvokeAsync` will automatically call `StartAsync` if you haven't called it explicitly. + + **When to call StartAsync explicitly:** + + - To inspect `InitStatus` before 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`: + +```csharp +[Fact] +public async Task Handler_WithInvalidInput_ReturnsStructuredError() +{ + await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(TestContext.Current.CancellationToken); + + var response = await factory.TestServer.InvokeAsync( + "", // Invalid input + TestContext.Current.CancellationToken + ); + + response.WasSuccess.Should().BeFalse(); + response.Error.Should().NotBeNull(); + response.Error.ErrorMessage.Should().Contain("Name is required"); + // Error.ErrorType and Error.StackTrace also available +} +``` + +### Testing Concurrent Invocations + +The test server handles concurrent invocations safely with FIFO ordering: + +```csharp +[Fact] +public async Task ConcurrentInvocations_AreHandledInOrder() +{ + await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(TestContext.Current.CancellationToken); + + // Launch multiple concurrent invocations + var tasks = Enumerable.Range(1, 10) + .Select(i => factory.TestServer.InvokeAsync( + i, + TestContext.Current.CancellationToken)) + .ToArray(); + + var responses = await Task.WhenAll(tasks); + + // All invocations succeed + responses.Should().AllSatisfy(r => r.WasSuccess.Should().BeTrue()); + + // Responses maintain FIFO order + responses.Select(r => r.Response) + .Should().ContainInOrder("Hello 1!", "Hello 2!", "Hello 3!", "Hello 4!", "Hello 5!", + "Hello 6!", "Hello 7!", "Hello 8!", "Hello 9!", "Hello 10!"); +} +``` + +### Testing Middleware + +Verify middleware behavior by inspecting response metadata or side effects: + +```csharp +[Fact] +public async Task CustomMiddleware_AddsExpectedHeaders() +{ + await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(TestContext.Current.CancellationToken); + + var response = await factory.TestServer.InvokeAsync( + new Request(), + TestContext.Current.CancellationToken + ); + + response.WasSuccess.Should().BeTrue(); + + // Verify middleware added metadata to response + response.Response.Headers.Should().ContainKey("X-Request-Id"); +} +``` + +### Testing Lifecycle Hooks + +#### OnInit That Signals Shutdown + +```csharp +[Fact] +public async Task OnInit_WhenReturningFalse_ShutsDownGracefully() +{ + var mockService = Substitute.For(); + mockService.Initialize().Returns(false); // Signal shutdown + + await using var factory = new LambdaApplicationFactory() + .WithHostBuilder(builder => + builder.ConfigureServices((_, services) => + { + services.RemoveAll(); + services.AddSingleton(mockService); + })); + + var initResult = await factory.TestServer.StartAsync( + TestContext.Current.CancellationToken + ); + + initResult.InitStatus.Should().Be(InitStatus.HostExited); +} +``` + +#### OnInit That Throws Exceptions + +```csharp +[Fact] +public async Task OnInit_WhenThrowingException_ReturnsInitError() +{ + var mockService = Substitute.For(); + mockService.Initialize().Throws(new Exception("Database unavailable")); + + await using var factory = new LambdaApplicationFactory() + .WithHostBuilder(builder => + builder.ConfigureServices((_, services) => + { + services.RemoveAll(); + services.AddSingleton(mockService); + })); + + var initResult = await factory.TestServer.StartAsync( + TestContext.Current.CancellationToken + ); + + initResult.InitStatus.Should().Be(InitStatus.InitError); + initResult.Error.ErrorMessage.Should().Contain("Database unavailable"); +} +``` + +#### OnShutdown Exception Handling + +```csharp +[Fact] +public async Task OnShutdown_WhenThrowingException_AggregatesExceptions() +{ + var mockService = Substitute.For(); + mockService.When(x => x.Cleanup()).Do(_ => throw new Exception("Cleanup failed")); + + await using var factory = new LambdaApplicationFactory() + .WithHostBuilder(builder => + builder.ConfigureServices((_, services) => + { + services.RemoveAll(); + services.AddSingleton(mockService); + })); + + await factory.TestServer.StartAsync(TestContext.Current.CancellationToken); + + var act = async () => await factory.TestServer.StopAsync( + TestContext.Current.CancellationToken + ); + + (await act.Should().ThrowAsync()) + .WithInnerException() + .WithMessage("*Cleanup failed*"); +} +``` + +### Inspecting Services After Invocation + +Resolve singleton services from `factory.TestServer.Services` to validate state: + +```csharp +[Fact] +public async Task AfterInvocation_CanInspectSingletonState() +{ + await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(TestContext.Current.CancellationToken); + + await factory.TestServer.InvokeAsync( + new Request("test"), + TestContext.Current.CancellationToken + ); + + // Inspect singleton services after invocation + var metricsCollector = factory.TestServer.Services + .GetRequiredService(); + + metricsCollector.InvocationCount.Should().Be(1); +} +``` + +### Alternative DI Containers + +Replace the default DI container with Autofac, DryIoc, or other containers: + +```csharp +[Fact] +public async Task WithAutofac_CustomContainerWorks() +{ + await using var factory = new LambdaApplicationFactory() + .WithHostBuilder(builder => + builder + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureContainer((_, containerBuilder) => + { + containerBuilder.RegisterType().As(); + })); + + var response = await factory.TestServer.InvokeAsync( + new Request(), + TestContext.Current.CancellationToken + ); + + response.WasSuccess.Should().BeTrue(); +} +``` + +### Custom JSON Serialization + +Override JSON serialization options to match your Lambda's configuration: + +```csharp +[Fact] +public async Task CustomJsonOptions_AreRespected() +{ + await using var factory = new LambdaApplicationFactory() + .WithHostBuilder(builder => + builder.ConfigureServices((_, services) => + { + services.Configure(options => + { + options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + }); + })); + + var response = await factory.TestServer.InvokeAsync( + new Request { UserName = "test" }, + TestContext.Current.CancellationToken + ); + + response.WasSuccess.Should().BeTrue(); +} +``` + +### Performance and Cold Start Testing + +Measure initialization and invocation performance: + +```csharp +[Fact] +public async Task ColdStart_InitCompletesWithinTimeout() +{ + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + await using var factory = new LambdaApplicationFactory() + .WithCancellationToken(cts.Token); + + var stopwatch = Stopwatch.StartNew(); + var initResult = await factory.TestServer.StartAsync(cts.Token); + stopwatch.Stop(); + + initResult.InitStatus.Should().Be(InitStatus.InitCompleted); + stopwatch.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(3)); +} +``` + +## 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. -- **Parallel invocations** – The test server is concurrency-safe; the concurrent invocation test in - the suite shows FIFO ordering. -- **Assert via DI** – Resolve services from `factory.TestServer.Services` to inspect metrics, - in-memory stores, or other state after invocations. -- **Error assertions** – Check `InvocationResponse.Error` for message and stack trace data; it - mirrors what the Lambda Runtime API returns. - **Runtime headers** – Responses include the same headers Lambda sends (`Lambda-Runtime-*` plus any `AdditionalHeaders` you 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. -!!! warning "Fixture reuse pitfalls" +!!! warning "Fixture Reuse Pitfalls" - Using `IClassFixture`/`ICollectionFixture` with a single `LambdaApplicationFactory` means 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. @@ -144,6 +435,14 @@ test-specific values. side effects. Choose one approach (per-test or shared fixture) for a given test class/collection and clean up via `DisposeAsync`/`StopAsync` when done. -Ready to go deeper? The MinimalLambda.Testing source (`src/MinimalLambda.Testing/`) and its unit -tests (`tests/MinimalLambda.Testing.UnitTests/`) contain more examples of host overrides, -cancellation, and error handling patterns. +## Complete Examples + +For comprehensive examples covering all scenarios, see the **[MinimalLambda.Testing test suite](https://github.com/j-d-ha/minimal-lambda/tree/main/tests/MinimalLambda.Testing.UnitTests)**: + +- `SimpleLambdaTests.cs` – Basic invocation patterns and concurrent invocations +- `DiLambdaTests.cs` – DI container replacement and lifecycle testing +- `NoEventLambdaTests.cs` – Configuration overrides and handlers without events +- `NoResponseLambdaTests.cs` – Fire-and-forget handlers + +The MinimalLambda.Testing source (`src/MinimalLambda.Testing/`) also contains additional examples of +host overrides, cancellation, and error handling patterns. From a56bba5f6b9a26e995cf8f55b734ab8ce69bdac8 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 14 Dec 2025 13:32:58 -0500 Subject: [PATCH 6/6] chore(build): bump version to 2.0.0-beta.6 - Updated the `` property in `Directory.Build.props` from 2.0.0-beta.5 to 2.0.0-beta.6. --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 258f0801..bf4cdeab 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 2.0.0-beta.5 + 2.0.0-beta.6 MIT