--- url: https://jasperfx.github.io/alba/guide/gettingstarted.md description: >- Install Alba, bootstrap your ASP.NET Core application with AlbaHost, and write your first in-memory HTTP integration test scenario. --- # Getting Started ## What is Alba? Alba is a class library that you use in combination with unit testing tools like [xUnit.Net](https://xunit.net), [NUnit](https://docs.nunit.org/) or [TUnit](https://tunit.dev) to author integration tests against ASP.NET Core HTTP endpoints. Alba *scenarios* actually exercise the full ASP.NET Core application by running HTTP requests through your ASP.NET system **in memory** using the built in [ASP.NET Core TestServer](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests). You can certainly write integration tests by hand using the lower level `TestServer` and `HttpClient`, but you'll write much less code with Alba. Moreover, Alba *scenarios* were meant to be declarative to maximize the readability of the integration tests, making those tests much more valuable as living technical documentation. ::: tip As of 9.0+, Alba only supports .NET 10.0 or greater. You can still use older versions of Alba to test previous versions of ASP.NET Core. ::: ## Using an AI Coding Agent If an AI coding agent (Claude Code, Copilot, Cursor, Codex and the like) is adding the tests, give it the Alba agent prompt. It condenses the setup rules, the scenario API and the common pitfalls of Alba 9 into one block of instructions. Agents can also fetch the prompt directly from [agent-prompt.md](https://jasperfx.github.io/alba/agent-prompt.md). * **Claude Code**: paste the prompt into the repository's `CLAUDE.md`, or into a project skill, so it loads with every session. * **Other agents**: put it in `AGENTS.md`, `.cursor/rules`, or the equivalent instructions file for the tool. * **One-off tasks**: paste it at the top of the task description together with the endpoints you want covered. When an agent needs more depth than the prompt, point it at [llms.txt](https://jasperfx.github.io/alba/llms.txt) (an index of every page) or [llms-full.txt](https://jasperfx.github.io/alba/llms-full.txt) (the whole documentation in one file). ## Alba Setup To get started with Alba, add a Nuget reference to the Alba library to your testing project that also references the ASP.NET Core project that you're going to be testing: ```bash dotnet add package Alba ``` In the following sections I'll show you how to bootstrap your ASP.NET Core system with Alba and start authoring specifications with the `AlbaHost` type. ## Initializing AlbaHost Alba supports both traditional `Startup.cs` projects and the `WebApplicationBuilder` minimal hosting approach. On .NET 10, `AlbaHost.For` works with both models. On .NET 11, `AlbaHost.For` requires a `WebApplicationBuilder` application; bootstrap a `Startup.cs`-style application through its `IHostBuilder` instead, as shown in [Alternative Bootstrapping Methods](bootstrapping.md). As an example, consider this very small ASP.NET Core application utilizing the new [Minimal API](https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api?tabs=visual-studio) approach: ```cs var builder = WebApplication.CreateBuilder(args); // Add services to the container. var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.MapGet("/", () => "Hello World!"); app.MapGet("/blowup", context => throw new Exception("Boo!")); app.MapPost("/json", (MyEntity entity) => entity); app.MapGet("/args", () => Results.Ok(args)); app.Run(); public record MyEntity(Guid Id, string MyValue); ``` snippet source | anchor **As this is a Minimal API project, you will need to allow your test project access to the internal types of your application under test**. You can do that by either using the [InternalsVisibleToAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.internalsvisibletoattribute) in your main application project, or use this within the project file of your application where "ProjectName.Tests" would be your testing project name: ```xml ``` We can now scaffold the `AlbaHost` using `AlbaHost.For`, where `T` is your applications entry point: ```cs await using var host = await AlbaHost.For(x => { x.ConfigureServices((context, services) => { services.AddSingleton(); }); }); ``` snippet source | anchor The `AlbaHost.For(Action configuration)` method uses `WebApplicationFactory` and all its magic static member trickery to intercept and run the implied `Program.Main()` method from the sample application above while also allowing you to customize the application configuration at testing time. The "T" in this case is only a marker type so that `WebApplicationFactory` can choose the correct entry assembly for the web application that is being tested by Alba. See [this blog post from Andrew Lock on the WebApplicationFactory mechanics](https://andrewlock.net/exploring-dotnet-6-part-6-supporting-integration-tests-with-webapplicationfactory-in-dotnet-6/) for more information. ::: tip `AlbaHost` is an expensive object to create, so you'll generally want to reuse it across tests. See the relevant guide for [xUnit](xunit.md), [NUnit](nunit.md) or [TUnit](tunit.md) ::: ## Configuring the Host Fluently Every bootstrapping method returns an `AlbaHostBuilder`. You can chain the Alba specific parts of the setup onto it as part of the construction. ASP.NET Core host customization such as service registrations stays in the configuration action passed to `AlbaHost.For`. ```cs await using var host = await AlbaHost.For(x => { // Service registrations and other ASP.NET Core host // customization belong in this action x.ConfigureServices(services => { // services.AddSingleton(); }); }) // Override configuration values. These are visible to the // application's own startup code .WithConfiguration("ConnectionStrings:Postgres", "MyOverriddenValue") // Apply Alba extensions .WithExtension(new AuthenticationStub().WithName("jeremy")); ``` snippet source | anchor Calls are applied in the order they are chained, so an extension chained later overrides what an earlier one registered. Configuration values take precedence over the application's own configuration files. On .NET 11, `AlbaHost.For` applies them to the `WebApplicationBuilder` as soon as it is created, before the application's own startup code runs, so a value read in `Program.cs` before `builder.Build()` already sees the override. On .NET 10, `AlbaHost.For` passes the values to the application as host configuration, which requires the application to call `WebApplication.CreateBuilder(args)`. Host-level keys such as `environment` and `contentRoot` cannot be changed this way, use `UseEnvironment` on the `Action` overload for those. ### Seeing the test from the application By default the application handles each request without the test's async context, like a real server would. `WithTestContextFlow()` runs each scenario's request in the test's context instead, so values the test or the test framework keep in an `AsyncLocal`, such as xUnit's `TestContext.Current`, the current `Activity` and the current culture, are visible to the application while it handles the request: ```cs await using var host = await AlbaHost.For() .WithTestContextFlow(); ``` A logger that looks up the current test for each message, such as one writing to xUnit's `TestContext.Current.TestOutputHelper`, then puts the application's logs in the test that sent the request. Each request also carries the trace context of the test's current `Activity`, including its baggage, in the request headers regardless of `WithTestContextFlow` being set or not. ## Running a Scenario Once you have a `AlbaHost` object, you're ready to execute *Scenario's* through your system inside of tests. Below is a scenario for the "hello, world" application: ```cs [Fact] public async Task should_say_hello_world() { // Alba will automatically manage the lifetime of the underlying host await using var host = await AlbaHost.For(); // This runs an HTTP request and makes an assertion // about the expected content of the response await host.Scenario(_ => { _.Get.Url("/"); _.ContentShouldBe("Hello World!"); _.StatusCodeShouldBeOk(); }); } ``` snippet source | anchor The `Action` argument will completely configure the ASP.NET `HttpContext` for the request and apply any of the declarative response assertions. The actual HTTP request happens inside of the `Scenario()` method. The `Scenario` response contains the raw `HttpContext` and several helper methods to help you parse or read information from the response body: ```cs [Fact] public async Task should_return_entity_assert_response() { await using var host = await AlbaHost.For(); var guid = Guid.NewGuid(); var res = await host.Scenario(_ => { _.Post.Json(new MyEntity(guid, "SomeValue")).ToUrl("/json"); _.StatusCodeShouldBeOk(); }); var json = await res.ReadAsJsonAsync(); Assert.Equal(guid, json.Id); } ``` snippet source | anchor If the existing *Scenario* assertions aren't enough to verify your test case, you can work directly against the raw response: ```cs [Fact] public async Task should_say_hello_world_with_raw_objects() { await using var host = await AlbaHost.For(); var response = await host.Scenario(_ => { _.Get.Url("/"); _.StatusCodeShouldBeOk(); }); // you can go straight at the HttpContext & do assertions directly on the responseStream Stream responseStream = response.Context.Response.Body; } ``` snippet source | anchor Do note that Alba quietly "rewinds" the `HttpContext.Response.Body` stream so that you can more readily read and work with the contents. ## Customizing the System for Testing You can configure your application with mocked services or test-specific configuration like so: ```cs var stubbedWebService = new StubbedWebService(); await using var host = await AlbaHost.For(x => { // override the environment if you need to x.UseEnvironment("Testing"); // override service registrations or internal options if you need to x.ConfigureServices(s => { s.AddSingleton(stubbedWebService); s.PostConfigure(o => o.SerializerSettings.TypeNameHandling = TypeNameHandling.All); }); }); host.BeforeEach(httpContext => { // do some data setup or clean up before every single test }) .AfterEach(httpContext => { // do any kind of cleanup after each scenario completes }); ``` snippet source | anchor Alba does not do anything to set the hosting environment, but you can do that yourself via the `IWebHostBuilder` ## Frequently Asked Questions ### How is Alba different from WebApplicationFactory? Alba builds on `WebApplicationFactory` and `TestServer`. Instead of building `HttpClient` requests and parsing responses by hand, you describe the request and the expected response in one scenario, and a failure reports the response body. Alba also adds authentication stubs, JSON and problem details assertions, host pooling for parallel tests and endpoint coverage reports. ### Which test frameworks does Alba work with? Any of them. The docs show how to share a host with [xUnit](xunit.md), [NUnit](nunit.md) and [TUnit](tunit.md). ### Which versions of .NET are supported? Alba 9 supports .NET 10 and .NET 11. Earlier versions of Alba support older versions of ASP.NET Core. ### Can I test applications that use authentication? Yes. [Security extensions](security.md) stub out authentication, mint JWT bearer tokens from your own configuration, or sign in against a real OpenID Connect server. ### Can AI coding agents write Alba tests? Yes. Give the agent the [Alba agent prompt](#using-an-ai-coding-agent), and point it at [llms.txt](https://jasperfx.github.io/alba/llms.txt) or [llms-full.txt](https://jasperfx.github.io/alba/llms-full.txt) for the full documentation. ### Is Alba free? Yes. Alba is licensed under Apache 2.0. [JasperFx Software](https://jasperfx.net) offers paid support and consulting. --- --- url: https://jasperfx.github.io/alba/guide/xunit.md description: >- Share one AlbaHost across xUnit tests with IAsyncLifetime class and collection fixtures to write fast ASP.NET Core integration tests. --- # Integrating with xUnit If you are writing only a few Alba specifications in your testing project and your application spins up very quickly, you can just happily write tests like this: ```cs [Fact] public async Task should_say_hello_world() { // Alba will automatically manage the lifetime of the underlying host await using var host = await AlbaHost.For(); // This runs an HTTP request and makes an assertion // about the expected content of the response await host.Scenario(_ => { _.Get.Url("/"); _.ContentShouldBe("Hello World!"); _.StatusCodeShouldBeOk(); }); } ``` snippet source | anchor Do note that your `[Fact]` method needs to be declared as `async Task` to ensure that xUnit finishes the specification before disposing the system or you'll get *unusual* behavior. Also note that you really need to dispose the `AlbaHost` to shut down your application and dispose any internal services that might be holding on to computer resources. ## Class Fixtures If your application startup time becomes a performance problem, and especially in larger test suites, you probably want to share the `AlbaHost` object between tests. xUnit helpfully provides the [class fixture feature](https://xunit.net/docs/shared-context) for just this use case. Build out your `AlbaHost` in a class like this. ASP.NET Core setup goes in the configuration action, and Alba specific setup such as configuration overrides and extensions is chained onto the builder before it is awaited (see [Configuring the Host Fluently](gettingstarted.md#configuring-the-host-fluently)): ```cs public class WebAppFixture : IAsyncLifetime { public IAlbaHost AlbaHost = null!; public async ValueTask InitializeAsync() { AlbaHost = await Alba.AlbaHost.For(builder => { // ASP.NET Core host setup such as service registrations }) // Alba specific setup such as configuration overrides and extensions .WithConfiguration("Alba:Fixture", "xUnit"); } public async ValueTask DisposeAsync() { await AlbaHost.DisposeAsync(); } } ``` snippet source | anchor Then in your actual xUnit fixture classes, implement the `IClassFixture` class like this: ```cs public class ContractTestWithAlba : IClassFixture { public ContractTestWithAlba(WebAppFixture app) { _host = app.AlbaHost; } private readonly IAlbaHost _host; ``` snippet source | anchor ## Collection Fixtures In the previous section, the `WebAppFixture` instance will only be shared between all the tests in the one `ContractTestWithAlba` class. To reuse the `IAlbaHost` across multiple test fixture classes, you'll need to use xUnit.Net's [Collection Fixture](https://xunit.net/docs/shared-context) concept. Still using `WebAppFixture`, we'll now need to have a marker collection class like this: ```cs [CollectionDefinition("scenarios")] public class ScenarioCollection : ICollectionFixture { } ``` snippet source | anchor As a convenience, I like to have a base class for all test fixture classes that will be using scenarios like this: ```cs [Collection("scenarios")] public abstract class ScenarioContext { protected ScenarioContext(WebAppFixture fixture) { Host = fixture.AlbaHost; } public IAlbaHost Host { get; } } ``` snippet source | anchor And then inherit from that `ScenarioContext` base class in actual test fixture classes: ```cs public class sample_integration_fixture : ScenarioContext { public sample_integration_fixture(WebAppFixture fixture) : base(fixture) { } [Fact] public Task happy_path() { return Host.Scenario(_ => { _.Get.Url("/fake/okay"); _.StatusCodeShouldBeOk(); }); } } ``` snippet source | anchor --- --- url: https://jasperfx.github.io/alba/guide/nunit.md description: >- Bootstrap one AlbaHost per NUnit test run with a SetUpFixture and reuse it across test fixtures for ASP.NET Core integration tests. --- # Integrating with NUnit When using Alba within NUnit testing projects, you probably want to reuse the `IAlbaHost` across tests and test fixtures because `AlbaHost` is relatively expensive to create (it's bootstrapping your whole application more than Alba itself is slow). To do that with NUnit, you could track a single `AlbaHost` on a static class like this one: ```cs [SetUpFixture] public class Application { [OneTimeSetUp] public async Task Init() { Host = await AlbaHost.For(); } public static IAlbaHost Host { get; private set; } // Make sure that NUnit will shut down the AlbaHost when // all the projects are finished [OneTimeTearDown] public void Teardown() { Host.Dispose(); } } ``` snippet source | anchor Any Alba specific setup, such as configuration overrides or extensions, is chained onto `AlbaHost.For()` before it is awaited (see [Configuring the Host Fluently](gettingstarted.md#configuring-the-host-fluently)). Then reference the `AlbaHost` in tests like this sample: ```cs public class sample_integration_fixture { [Test] public async Task happy_path() { await Application.Host.Scenario(_ => { _.Get.Url("/fake/okay"); _.StatusCodeShouldBeOk(); }); } } ``` snippet source | anchor ## Running tests in parallel with a host pool Tests that change data can't safely share one host while they run in parallel. An [`AlbaHostPool`](pooling.md) lends each test a host of its own. Create the pool in a `[SetUpFixture]`, and dispose it in its `[OneTimeTearDown]`: ```cs // Applies to the tests in this namespace [SetUpFixture] public class PooledApplication { public static AlbaHostPool Pool { get; private set; } [OneTimeSetUp] public void CreatePool() { Pool = new AlbaHostPool(slot => AlbaHost.For() // Each host gets its own database .WithConfiguration("ConnectionStrings:Db", $"Host=localhost;Database=app_{slot.Index}"), new AlbaHostPoolOptions { // Hosts start as tests need them, up to this many MaxHosts = 4 }); } // Runs after every test in the namespace has finished [OneTimeTearDown] public async Task DisposePool() { await Pool.DisposeAsync(); } } ``` snippet source | anchor Then rent a host in `[SetUp]` and return it in `[TearDown]`. NUnit runs tests one at a time unless they are marked `[Parallelizable]`, and it shares one fixture instance between a class's tests unless the class uses `[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]`. Without that attribute, tests running in parallel would overwrite each other's lease: ```cs // A new instance for each test, so tests running in parallel each keep their own lease [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] [Parallelizable(ParallelScope.Children)] public class pooled_math_tests { private AlbaHostLease _host; [SetUp] public async Task RentHost() { // Name the test, because timeout messages would otherwise name this method _host = await PooledApplication.Pool.RentAsync(TestContext.CurrentContext.CancellationToken, caller: TestContext.CurrentContext.Test.Name); } // Resets the host and returns it to the pool [TearDown] public async Task ReturnHost() { if (_host != null) await _host.DisposeAsync(); } [TestCase(1, 2, 3)] [TestCase(2, 3, 5)] [TestCase(10, 20, 30)] public async Task adds_numbers(int one, int two, int sum) { await _host.Scenario(x => { x.Get.Url($"/math/add/{one}/{two}"); x.JsonPath("$.answer").ShouldBe(sum); }); } } ``` snippet source | anchor --- --- url: https://jasperfx.github.io/alba/guide/tunit.md description: >- Share an AlbaHost across TUnit tests with IAsyncInitializer and ClassDataSource to run ASP.NET Core integration tests. --- # Integrating with TUnit Like other testing frameworks, you'll want to reuse the `IAlbaHost` across tests and test fixtures because `AlbaHost` is relatively expensive to create. To do that with TUnit, you should start by writing a bootstrapping class that inherits from `IAsyncInitializer` and `IAsyncDisposable`: ```cs public sealed class AlbaBootstrap : IAsyncInitializer, IAsyncDisposable { public IAlbaHost Host { get; private set; } = null!; public async Task InitializeAsync() { Host = await AlbaHost.For(); } public async ValueTask DisposeAsync() { await Host.DisposeAsync(); } } ``` snippet source | anchor Any Alba specific setup, such as configuration overrides or extensions, is chained onto `AlbaHost.For()` before it is awaited (see [Configuring the Host Fluently](gettingstarted.md#configuring-the-host-fluently)). Then inject the instance by adding `[ClassDataSource(Shared = SharedType.PerTestSession)]` to your test class. We recommend creating a base class to allow easier access of the host and any other dependencies. ```cs public abstract class AlbaTestBase(AlbaBootstrap albaBootstrap) { protected IAlbaHost Host => albaBootstrap.Host; } [ClassDataSource(Shared = SharedType.PerTestSession)] public class MyTestClass(AlbaBootstrap albaBootstrap) : AlbaTestBase(albaBootstrap) { [Test] public async Task happy_path() { await Host.Scenario(_ => { _.Get.Url("/fake/okay"); _.StatusCodeShouldBeOk(); }); } } ``` snippet source | anchor ## Isolating parallel tests with a host pool TUnit runs tests in parallel by default, so tests that change data can't safely share one host. An [`AlbaHostPool`](pooling.md) lends each test a host of its own. Share the pool for the test session: ```cs public sealed class AlbaPool : IAsyncDisposable { public const int Hosts = 4; public AlbaHostPool Pool { get; } = new(slot => AlbaHost.For() // Each host gets its own database .WithConfiguration("ConnectionStrings:Db", $"Host=localhost;Database=app_{slot.Index}"), new AlbaHostPoolOptions { // Hosts start as tests need them, up to this many MaxHosts = Hosts }); public ValueTask DisposeAsync() => Pool.DisposeAsync(); } // TUnit runs no more of the tests marked with this limiter at once than the pool has hosts public sealed class PooledHostsLimit : IParallelLimit { public int Limit => AlbaPool.Hosts; } ``` snippet source | anchor Then rent a host for each test. TUnit creates the test class for each test, so a base class can rent in `InitializeAsync` and return the host in `DisposeAsync`: ```cs // TUnit creates the test class for each test, so each test rents its own host public abstract class PooledTestBase(AlbaPool pool) : IAsyncInitializer, IAsyncDisposable { private AlbaHostLease? _lease; protected IAlbaHost Host => _lease!; public async Task InitializeAsync() { var test = TestContext.Current; // Name the test, because timeout messages would otherwise name this method _lease = await pool.Pool.RentAsync(test?.Execution.CancellationToken ?? default, caller: test?.Metadata.TestName ?? ""); } // Resets the host and returns it to the pool public async ValueTask DisposeAsync() { if (_lease is not null) await _lease.DisposeAsync(); } } [ClassDataSource(Shared = SharedType.PerTestSession)] [ParallelLimiter] public class PooledMathTests(AlbaPool pool) : PooledTestBase(pool) { [Test] [Arguments(1, 2, 3)] [Arguments(2, 3, 5)] [Arguments(10, 20, 30)] public async Task adds_numbers(int one, int two, int sum) { await Host.Scenario(x => { x.Get.Url($"/math/add/{one}/{two}"); x.JsonPath("$.answer").ShouldBe(sum); }); } } ``` snippet source | anchor `[ParallelLimiter]` stops TUnit running more of these tests at once than the pool has hosts, so a test doesn't start only to wait for a host. The limit is shared by every class marked with the same limiter type. TUnit disposes the `AlbaPool`, and with it every host, after the test session ends. Shared infrastructure such as a database container can be injected into the class that owns the pool, as a property with its own `[ClassDataSource]`. TUnit initializes it before the owning class, so create the pool in `InitializeAsync`: ```cs public sealed class AlbaPool : IAsyncInitializer, IAsyncDisposable { // Startup the containers before the test pool [ClassDataSource(Shared = SharedType.PerTestSession)] public required PostgresContainer Database { get; init; } public AlbaHostPool Pool { get; private set; } = null!; public Task InitializeAsync() { Pool = new AlbaHostPool(slot => AlbaHost.For() .WithConfiguration("ConnectionStrings:Db", Database.ConnectionStringFor($"app_{slot.Index}")), new AlbaHostPoolOptions { MaxHosts = 4 }); return Task.CompletedTask; } public ValueTask DisposeAsync() => Pool.DisposeAsync(); } ``` Tests that create their own data instead of relying on a reset can name it with `TestContext.Current!.Isolation.GetIsolatedPrefix()`, which is unique to each test, for example as a tenant id. ## Application output and logs Output the application writes with `Console` while it handles a scenario appears in the output of the test that sent the request. TUnit traces each test, Alba sends that trace with the request, and TUnit uses it to find the test. The default console logger writes from a background thread, so its messages don't reach the test. To see the application's `ILogger` output in each test, add the `TUnit.Logging.Microsoft` package and register its logger in the host's services: ```cs // Writes the application's logs to the test that sent the request builder.Services.AddCorrelatedTUnitLogging(); ``` snippet source | anchor --- --- url: https://jasperfx.github.io/alba/guide/pooling.md description: >- Run ASP.NET Core integration tests in parallel with AlbaHostPool, which lends each test its own host and resets it between tests. --- # Pooling Hosts for Parallel Tests Sharing one `AlbaHost` keeps a test suite fast, but tests that change data or stubbed responses can't run against it at the same time. `AlbaHostPool` runs several hosts of the application and lends each one to a single test at a time. Tests run in parallel, each one has a host to itself, and every host is reset before the next test rents it. ## Creating a pool The pool calls a factory each time it needs a host. The factory gets the host's `AlbaHostSlot`, whose `Index` runs from 0 to one less than `MaxHosts`, so each host can be given its own database or other infrastructure. The pool types are in the `Alba.Pooling` namespace. ```cs // Registered once per test assembly with [assembly: AssemblyFixture] public class ApiHosts : IAsyncLifetime { public AlbaHostPool Pool { get; } = new(slot => AlbaHost.For() // Each host gets its own database .WithConfiguration("ConnectionStrings:Db", $"Host=localhost;Database=app_{slot.Index}"), new AlbaHostPoolOptions { // Hosts start as tests need them, up to this many MaxHosts = 4 }); public ValueTask InitializeAsync() => ValueTask.CompletedTask; public ValueTask DisposeAsync() => Pool.DisposeAsync(); } ``` snippet source | anchor Hosts start as tests need them, so a suite that never runs more than two tests at once only starts two hosts. `MaxHosts` defaults to the processor count, which is also xUnit's default number of tests that run in parallel. Each host holds a whole application, with its own caches and connection pools, plus whatever runs per host such as a database. For a heavy application, start with half the processor count and measure. Create one pool for the test assembly and dispose it after the last test: * **xUnit**: an assembly fixture, as above, registered with `[assembly: AssemblyFixture]` and taken as a constructor argument by the test classes. * **NUnit**: a static pool created in the `[OneTimeSetUp]` of a `[SetUpFixture]` and disposed in its `[OneTimeTearDown]`. NUnit runs tests one at a time unless they are marked `[Parallelizable]`. See the [NUnit example](nunit.md#running-tests-in-parallel-with-a-host-pool). * **TUnit**: a class that owns the pool and implements `IAsyncDisposable`, injected with `[ClassDataSource(Shared = SharedType.PerTestSession)]`. TUnit runs every test in parallel, so add a `[ParallelLimiter]` whose limit is `MaxHosts`. See the [TUnit example](tunit.md#isolating-parallel-tests-with-a-host-pool). ## Renting a host A test rents a host, uses it alone, and disposes the lease to return it: ```cs public class PooledHostTests(ApiHosts hosts) { [Fact] public async Task say_hello() { // Disposing the lease resets the host and returns it to the pool await using var host = await hosts.Pool.RentAsync(TestContext.Current.CancellationToken); await host.Scenario(x => { x.Get.Url("/"); x.ContentShouldBe("Hello World!"); }); } } ``` snippet source | anchor The lease is an `IAlbaHost`, so scenarios and helpers such as `GetAsJson` work unchanged. Always dispose it with `await using`. A lease that is never disposed prevents other tests from using that host. * `BeforeEach`, `AfterEach` and their async versions called on the lease only apply until the lease is disposed. Actions that every test needs belong in the factory or in `OnCreated`. * `host.GetExtension()` finds the rented host's own extension. Pass a match when the host has several of the same type. * `host.Slot` is the rented host's slot, for reaching its own resources from the test. * Using a lease after it is disposed throws an `ObjectDisposedException`. ## Waiting for a host When every host is rented, `RentAsync` waits for one to be returned. Waiting tests get hosts in the order they asked. * `RentTimeout` (two minutes by default) fails a rent when no host has been returned to the pool for that long. The wait starts again each time any host comes back, so a long queue of tests, such as TUnit starting every test at once, does not time out while the pool keeps moving. The `TimeoutException` names each lease not yet returned and the test that rented it, which finds a test that forgot to dispose its lease. * The test is named from the method that calls `RentAsync`. When a base class or fixture rents for each test, pass the test's name instead, such as `pool.RentAsync(cancellation, caller: TestContext.Current.Test?.TestDisplayName ?? "")` in xUnit. * `RentAsync` takes a cancellation token, such as xUnit's `TestContext.Current.CancellationToken`. * Rent one host per test. A test that rents a second host while holding one can wait forever when the pool is full. * `WarmUpAsync()` starts hosts in parallel ahead of the first tests, so a startup failure surfaces in the fixture rather than in a test. * `MaxConcurrentStarts` limits how many hosts start at once. Set it to 1 when the application's startup code can't run for several hosts in one process at the same time, for example because it creates databases or takes a lock in a shared scheduler. When a host fails to start, the test that started it gets the exception and every later rent fails with an `InvalidOperationException` wrapping it, so a broken application fails the suite once rather than once per test. Set `FailFastOnStartupFailure = false` to try again on each rent instead. ## Resetting between tests Disposing a lease resets the host in this order: 1. `BeforeEach` and `AfterEach` actions added since `OnCreated` ran, through the lease or in `OnRent`, are removed. 2. Extensions that implement `IResettableAlbaExtension` go back to their state once `OnCreated` ran. Implement it on an extension of your own that holds state for one host, such as a fake of another service: `Checkpoint()` records the state and `ResetAsync()` returns to it. 3. `OnReturn` runs, to remove the data the test created. ```cs new AlbaHostPoolOptions { // Once per host, before its first test OnCreated = async (host, slot) => { using var scope = host.Services.CreateScope(); await scope.ServiceProvider.GetRequiredService().Database.MigrateAsync(); }, // After every test, for example with Marten OnReturn = (host, slot) => host.ResetAllMartenDataAsync() } ``` `OnRent` runs before each test gets the host, for example to seed data. When `OnRent` or `OnReturn` throws, the pool disposes that host and starts a new one in the same slot when it is next needed. A failed `OnReturn` is thrown from disposing the lease, so the test that left the host in a bad state is the one that fails. Call `host.Discard()` when a test changes something that resetting does not undo, such as a singleton's state. The pool disposes that host when the lease is disposed and replaces it in the same slot. Tests that create unique data for themselves, such as new ids or tenants, may not need any reset. That is the cheapest option when the application allows it. `TimeProviderOverride` is not reset, because its clock can only move forward. Tests that depend on the current time should set it with `SetUtcNow(...)` to a time later than any earlier test used. A test harness that is started once per host and reused, such as MassTransit's `ITestHarness`, keeps what it recorded from earlier tests. Look for messages by values the current test created, and raise inactivity timeouts such as MassTransit's `TestHarnessOptions.TestInactivityTimeout` past the length of the test run, because once they expire they don't reset. ## Keeping state with each host Tests often need more than the host: a mock that isn't registered in DI, a `DbContext` pointed at the host's database, or users seeded when the host started. `AlbaHostPool` keeps an object of your own with each host. The factory returns the host and its state, the lifecycle actions receive the state, and `lease.State` gives it to the test: ```cs public sealed class ApiInstance(string connectionString) : IAsyncDisposable { public string ConnectionString { get; } = connectionString; public Mock ChatClient { get; } = new(); public Profile Administrator { get; set; } = null!; public ValueTask DisposeAsync() => ValueTask.CompletedTask; } var pool = new AlbaHostPool(async (slot, cancellation) => { var instance = new ApiInstance(await TestDatabases.CreateAsync($"app_{slot.Index}", cancellation)); var host = await AlbaHost.For(x => x.ConfigureServices(s => s.AddSingleton(instance.ChatClient.Object))) .WithConfiguration("ConnectionStrings:Db", instance.ConnectionString); return (host, instance); }, new AlbaHostPoolOptions { OnCreated = async (host, instance) => instance.Administrator = await Seed.AdministratorAsync(host), OnRent = (host, instance) => TestDatabases.RestoreSeedAsync(instance.ConnectionString) }); await using var host = await pool.RentAsync(cancellation); host.State.ChatClient.Setup(...); ``` When the state implements `IAsyncDisposable` or `IDisposable`, the pool disposes it just before its host. The pool disposes the host itself, so the state doesn't need to. ## Separate infrastructure per host Derive each host's resources from its slot. A replacement host gets the same index, so it reuses the same resources. | Resource | Separate it by | |---|---| | Relational database | One database per slot on a shared server or container, such as `app_{slot.Index}` | | Marten or EF Core | One schema per slot, which is cheaper than separate databases | | Message broker | A queue or topic prefix per slot, from `slot.Name` | | Files | A directory per slot | When resources must exist before the host starts, use the factory that returns the started host: ```cs var pool = new AlbaHostPool(async (slot, cancellation) => { var database = $"app_{slot.Index}"; await TestDatabases.CreateAsync(database, cancellation); // Disposed after this host is disposed slot.RegisterForDispose(new DropDatabase(database)); return await AlbaHost.For() .WithConfiguration("ConnectionStrings:Db", TestDatabases.ConnectionStringFor(database)); }); ``` The pool runs the factory again when it replaces a host: after `Discard()`, after a failed `OnRent`, `OnReturn` or extension reset, and after a startup failure when `FailFastOnStartupFailure` is off. So the factory must work when run more than once for the same slot. Registering the cleanup with `slot.RegisterForDispose` is enough when the cleanup can't fail. Otherwise create the resource only if it is missing, or name it with `slot.Generation`, which counts the hosts the slot held before, such as `app_{slot.Index}_{slot.Generation}`. Create extensions that hold state for one host inside the factory. The pool throws if one `IResettableAlbaExtension` instance is passed to two hosts, because returning one host would reset the other. Extensions meant to be shared, such as [`EndpointCoverage`](endpoints.md), can be passed to every host. Hosts in a pool share one process, so static state in the application, such as a static cache or Serilog's `Log.Logger`, is shared by every host. So is anything the application reads from process-wide sources, such as environment variables or a `.env` file loaded after the test configuration. Give each host its own value through `WithConfiguration` or a service registration in the factory instead. ## Tracing the pool The pool records activities on Alba's `"Alba"` `ActivitySource`, alongside the scenario activities described in [Tracing & Open Telemetry](opentelemetry.md): | Activity | Covers | Tags | |---|---|---| | `AlbaHostPool rent` | A rent, from waiting for a host to `OnRent` | `alba.pool.slot`, `alba.pool.wait_ms` | | `AlbaHostPool start host` | Starting a host, including `OnCreated` | `alba.pool.slot`, `alba.pool.slot_generation` | | `AlbaHostPool return` | Resetting a returned host, including `OnReturn` | `alba.pool.slot`, `alba.pool.discarded` | They show where a slow suite spends its time: waiting for hosts, starting them or resetting them. A failure marks the activity with an error status. --- --- url: https://jasperfx.github.io/alba/guide/aspire.md description: >- Start real databases and message brokers from an Aspire AppHost with Aspire.Hosting.Testing and pass their connection strings to AlbaHost. --- # Aspire Hosting Alba runs your application in process, but the application usually still needs real infrastructure such as a database or a message broker. If the system already has an [Aspire](https://aspire.dev) AppHost, the test suite can reuse it through the `Aspire.Hosting.Testing` package. The AppHost starts those dependencies in Docker once for the whole test assembly, and their connection strings flow into the `AlbaHost` configuration. The samples on this page come from the `src/AspireSamples` project in the Alba repository. They use xUnit 4, Postgres and RabbitMQ, and they need Docker to run. ## The AppHost This is an ordinary AppHost for an API that depends on a Postgres database and a RabbitMQ broker: ```cs var builder = DistributedApplication.CreateBuilder(args); var ledger = builder.AddPostgres("postgres").AddDatabase("ledger"); var messaging = builder.AddRabbitMQ("messaging"); // WithReference hands the API ConnectionStrings:ledger and ConnectionStrings:messaging builder.AddProject("api") .WithReference(ledger).WaitFor(ledger) .WithReference(messaging).WaitFor(messaging); builder.Build().Run(); ``` snippet source | anchor The tests reuse this AppHost as it is. `WithReference` gives the API `ConnectionStrings:ledger` and `ConnectionStrings:messaging`. The Alba fixture below supplies the same configuration keys, so the application code does not change between running under Aspire and running under Alba. ## Starting the AppHost once per test assembly Add a package reference to `Aspire.Hosting.Testing` and a project reference to the AppHost in the test project. An xUnit assembly fixture then starts the AppHost before the first test and stops it after the last one: ```cs public class AspireInfrastructure : IAsyncLifetime { private DistributedApplication? _app; public string LedgerConnectionString { get; private set; } = null!; public string MessagingConnectionString { get; private set; } = null!; public async ValueTask InitializeAsync() { // The first run pulls the container images, so allow for a slow start using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(5)); var appHost = await DistributedApplicationTestingBuilder .CreateAsync(timeout.Token); // Keep container logs, and the health check failures logged while // the containers start, out of the test output appHost.Services.AddLogging(logging => logging .SetMinimumLevel(LogLevel.Warning) .AddFilter("AspireSamples.AppHost.Resources", LogLevel.None) .AddFilter("Microsoft.Extensions.Diagnostics.HealthChecks", LogLevel.None)); // Alba runs the API in process, so Aspire only starts its dependencies appHost.Resources.Remove(appHost.Resources.Single(x => x.Name == "api")); _app = await appHost.BuildAsync(timeout.Token); await _app.StartAsync(timeout.Token); // Wait on the database rather than the server, which is healthy before the database exists await _app.ResourceNotifications.WaitForResourceHealthyAsync("ledger", timeout.Token); await _app.ResourceNotifications.WaitForResourceHealthyAsync("messaging", timeout.Token); LedgerConnectionString = (await _app.GetConnectionStringAsync("ledger", timeout.Token))!; MessagingConnectionString = (await _app.GetConnectionStringAsync("messaging", timeout.Token))!; } public async ValueTask DisposeAsync() { // Stops the AppHost and removes its containers if (_app != null) await _app.DisposeAsync(); } } ``` snippet source | anchor * `DistributedApplicationTestingBuilder.CreateAsync()` runs the AppHost with defaults suited to testing. The `Projects.AspireSamples_AppHost` type is generated from the project reference. * The API resource is removed before the AppHost starts, because Alba runs the API in process instead. Aspire only starts the containers. * `WaitForResourceHealthyAsync` waits until a resource's health checks pass. Wait on the database resource rather than the Postgres server: the server reports healthy before Aspire has created the database. * `GetConnectionStringAsync` returns the connection string of the running container, including its generated password and mapped port. * Disposing the `DistributedApplication` stops the AppHost and removes its containers. Register the fixture once for the assembly: ```cs // One AppHost, and one set of containers, for every test in the assembly [assembly: AssemblyFixture] ``` snippet source | anchor ## Sharing the AppHost with Alba fixtures xUnit injects assembly fixtures into collection fixture constructors, so the fixture that owns the `AlbaHost` receives the running infrastructure and passes the connection strings to the application with `WithConfiguration`: ```cs public class ApiFixture(AspireInfrastructure infrastructure) : IAsyncLifetime { public IAlbaHost Host { get; private set; } = null!; public async ValueTask InitializeAsync() { // The same configuration keys that WithReference() sets when Aspire runs the API Host = await AlbaHost.For() .WithConfiguration("ConnectionStrings:ledger", infrastructure.LedgerConnectionString) .WithConfiguration("ConnectionStrings:messaging", infrastructure.MessagingConnectionString); } public async ValueTask DisposeAsync() { await Host.DisposeAsync(); } } [CollectionDefinition(nameof(ApiCollection))] public class ApiCollection : ICollectionFixture; ``` snippet source | anchor Any number of Alba fixtures, collections and test classes can take `AspireInfrastructure` the same way, and they all share the one AppHost and its containers. xUnit disposes collection fixtures before assembly fixtures, so every `AlbaHost` shuts down before the containers it uses are removed. ## Writing scenarios Scenarios run against the real database and broker. Test classes can also take `AspireInfrastructure` directly to check side effects at the source, as the second test does with the RabbitMQ queue: ```cs [Collection(nameof(ApiCollection))] public class deposit_specs(ApiFixture api) { [Fact] public async Task deposits_add_up_to_the_balance() { // The database is shared by every test, so each test uses its own account var accountId = Guid.NewGuid(); await api.Host.Scenario(x => { x.Post.Json(new Deposit(accountId, 100m)).ToUrl("/deposits"); x.StatusCodeShouldBe(201); }); await api.Host.Scenario(x => { x.Post.Json(new Deposit(accountId, 50m)).ToUrl("/deposits"); x.StatusCodeShouldBe(201); }); var balance = await api.Host.GetAsJson($"/accounts/{accountId}/balance"); balance!.Amount.ShouldBe(150m); } } [Collection(nameof(ApiCollection))] public class deposit_messaging_specs(ApiFixture api, AspireInfrastructure infrastructure) { [Fact] public async Task every_deposit_is_published_to_the_queue() { var deposit = new Deposit(Guid.NewGuid(), 25m); await api.Host.Scenario(x => { x.Post.Json(deposit).ToUrl("/deposits"); x.StatusCodeShouldBe(201); }); // Read the broker directly with the connection string the API was given var factory = new ConnectionFactory { Uri = new Uri(infrastructure.MessagingConnectionString) }; await using var connection = await factory.CreateConnectionAsync(TestContext.Current.CancellationToken); await using var channel = await connection.CreateChannelAsync(cancellationToken: TestContext.Current.CancellationToken); (await ReceiveAsync(channel, deposit.AccountId)).ShouldBe(deposit); } // Other tests publish to the same queue, so skip past their deposits private static async Task ReceiveAsync(IChannel channel, Guid accountId) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); while (!timeout.IsCancellationRequested) { var message = await channel.BasicGetAsync(DepositPublisher.QueueName, autoAck: true, timeout.Token); if (message == null) { await Task.Delay(100, timeout.Token); continue; } var deposit = JsonSerializer.Deserialize(message.Body.Span); if (deposit?.AccountId == accountId) return deposit; } return null; } } ``` snippet source | anchor Every test shares the same containers, so give each test its own data, such as a fresh account id here, instead of relying on an empty database. ## Tips * The first run pulls the container images, which can take several minutes. The fixture above allows five minutes for startup. Later runs start in seconds. * On .NET 10, `AlbaHost.For` passes `WithConfiguration` values to the application as host configuration, so the application must call `WebApplication.CreateBuilder(args)`. See [Configuring the Host Fluently](gettingstarted.md#configuring-the-host-fluently). * Build agents need Docker. GitHub's hosted Linux runners have it. * With NUnit or TUnit, start the AppHost in a [`[SetUpFixture]`](nunit.md) or in a [`ClassDataSource`](tunit.md) shared per test session, then build the `AlbaHost` from its connection strings the same way. --- --- url: https://jasperfx.github.io/alba/guide/extensions.md description: >- Customize AlbaHost with IAlbaExtension, including the built-in TimeProviderOverride for controlling time in ASP.NET Core integration tests. --- # Extension Model Alba has an extension model based on this interface: ```cs /// /// Models an extension to an AlbaHost /// public interface IAlbaExtension : IDisposable, IAsyncDisposable { /// /// Called during the initialization of an AlbaHost after the application is started, /// so the application DI container is available. Useful for registering setup or teardown /// actions on an AlbaHost /// /// /// Task Start(IAlbaHost host); /// /// Allow an extension to alter the application under test before it starts. /// Behaves identically for every AlbaHost bootstrapping style. /// /// void Configure(IAlbaHostBuilder builder) { } } /// /// Hosting-model-agnostic configuration surface handed to Alba extensions /// before the application under test starts /// public interface IAlbaHostBuilder { /// /// Add or replace services in the application under test /// void ConfigureServices(Action configure); /// /// Add configuration sources for the application under test. Sources added /// here take precedence over the application's own configuration. /// void ConfigureConfiguration(Action configure); } ``` snippet source | anchor `Configure` runs before the application under test starts and behaves the same regardless of how the `AlbaHost` was bootstrapped (`IHostBuilder`, `WebApplicationBuilder`, or `WebApplicationFactory`). It has a default no-op implementation, so extensions that only need post-start logic can implement `Start` alone. Extensions are applied when the `AlbaHost` is bootstrapped, either as arguments to any of the `AlbaHost.For(...)` methods or chained onto the returned builder with `WithExtension(...)`. This sample from the security stub testing passes one as an argument: ```cs // This is a Alba extension that can "stub" out authentication var securityStub = new AuthenticationStub() .With("foo", "bar") .With(JwtRegisteredClaimNames.Email, "guy@company.com") .WithName("jeremy"); // We're calling your real web service's configuration theHost = await AlbaHost.For(securityStub); ``` snippet source | anchor The same kind of extension chained onto the builder: ```cs // Stub out an individual scheme var securityStub = new AuthenticationStub("custom") .With("foo", "bar") .With(JwtRegisteredClaimNames.Email, "guy@company.com") .WithName("jeremy"); await using var host = await AlbaHost.For() .WithExtension(securityStub); ``` snippet source | anchor See [Configuring the Host Fluently](gettingstarted.md#configuring-the-host-fluently) for the full builder surface. ## Time Travel with TimeProviderOverride Time-sensitive endpoints — token or session expiry, cache TTLs, scheduling, "valid until" business rules — need a deterministic clock. The `TimeProviderOverride` extension, in the `Alba.Extensions` namespace, replaces the application's `TimeProvider` registration with a controllable [FakeTimeProvider](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.time.testing.faketimeprovider) on every bootstrapping style. The extension *is* the clock: pass it to `AlbaHost.For(...)`, then drive time from the test. ```cs [Fact] public async Task freezing_and_advancing_the_clock() { // The clock is both the extension and the controllable TimeProvider var clock = new TimeProviderOverride(new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero)); await using var host = await AlbaHost.For(clock); // Time is frozen at the configured start instant (await host.GetAsJson("/time"))!.UtcNow .ShouldBe(new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero)); (await host.GetAsJson("/time"))!.UtcNow .ShouldBe(new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero)); // Travel forward three days clock.Advance(TimeSpan.FromDays(3)); (await host.GetAsJson("/time"))!.UtcNow .ShouldBe(new DateTimeOffset(2030, 1, 4, 0, 0, 0, TimeSpan.Zero)); // Or pin the clock to any instant clock.SetUtcNow(new DateTimeOffset(2031, 6, 15, 12, 0, 0, TimeSpan.Zero)); (await host.GetAsJson("/time"))!.UtcNow .ShouldBe(new DateTimeOffset(2031, 6, 15, 12, 0, 0, TimeSpan.Zero)); } ``` snippet source | anchor The clock is frozen: `GetUtcNow()` returns the same instant until the test moves it with `Advance(...)` or `SetUtcNow(...)`, or opts into automatic movement via `AutoAdvanceAmount`. Timers created from the provider (including `Task.Delay(..., timeProvider)` and cache expirations) fire when `Advance` crosses their due time. The override only affects code that resolves `TimeProvider` from the container. Calls to `DateTime.UtcNow` or a directly captured `TimeProvider.System` are not intercepted — that is inherent to the `TimeProvider` pattern, not specific to Alba. The extension itself doubles as a compact example of the extension model: ```cs /// /// Alba extension that replaces the application's registration /// with this controllable for time-travel testing. Pass an /// instance to AlbaHost.For(...), then drive time from the test with Advance(...) and /// SetUtcNow(...). Time is frozen unless AutoAdvanceAmount is set. /// public sealed class TimeProviderOverride : FakeTimeProvider, IAlbaExtension { /// /// Starts the clock at FakeTimeProvider's default of 2000-01-01T00:00:00Z /// public TimeProviderOverride() { } /// /// Starts the clock at the supplied instant /// public TimeProviderOverride(DateTimeOffset startDateTime) : base(startDateTime) { } void IAlbaExtension.Configure(IAlbaHostBuilder builder) { builder.ConfigureServices(services => { services.RemoveAll(); services.AddSingleton(this); }); } Task IAlbaExtension.Start(IAlbaHost host) { return Task.CompletedTask; } void IDisposable.Dispose() { } ValueTask IAsyncDisposable.DisposeAsync() { return ValueTask.CompletedTask; } } ``` snippet source | anchor --- --- url: https://jasperfx.github.io/alba/guide/security.md description: >- Stub ASP.NET Core authentication in Alba tests with AuthenticationStub and JwtSecurityStub, set claims per scenario, or use real OpenID Connect tokens. --- # Security Extensions ## Stub out all authentication To stub out all possible authentication inside your ASP.NET Core system, you can use the `AuthenticationStub` to automatically authenticate every request and build out a `ClaimsPrincipal` to your specification. Here's a sample of bootstrapping an `AlbaHost` with the `AuthenticationStub`. Security extensions can be passed as arguments to `AlbaHost.For(...)` as shown here, or chained onto the returned builder with `WithExtension(...)`: ```cs // This is a Alba extension that can "stub" out authentication var securityStub = new AuthenticationStub() .With("foo", "bar") .With(JwtRegisteredClaimNames.Email, "guy@company.com") .WithName("jeremy"); // We're calling your real web service's configuration theHost = await AlbaHost.For(securityStub); ``` snippet source | anchor When you need to test scenarios with different claims than the "baseline" claims defined on the `AuthenticationStub` object you created in the `AlbaHost` setup, you can use the `WithClaim()` method as shown below to add additional claims to the principal for only one scenario as shown below: ```cs [Fact] public async Task can_modify_claims_per_scenario() { var input = new Numbers { Values = new[] {2, 3, 4} }; var response = await theHost.Scenario(x => { // This is a custom claim that would only be used for the // JWT token in this individual test x.WithClaim(new Claim("color", "green")); x.RemoveClaim("foo"); x.Post.Json(input).ToUrl("/math"); x.StatusCodeShouldBeOk(); }); var principal = response.Context.User; principal.ShouldNotBeNull(); principal.Claims.Single(x => x.Type == "color") .Value.ShouldBe("green"); principal.Claims.Any(x => x.Type.Equals("foo")).ShouldBeFalse(); } ``` snippet source | anchor ## Stub out JWT authentication If you have an application that uses JWT, bearer tokens as your primary means of authentication, you can use the new `JwtSecurityStub` to automatically add a valid JWT token string to the HTTP `Authorization` header before any scenarios are executed. Similar to the `AuthenticationStub`, the `JwtSecurityStub` allows you to specify a baseline set of claims that should be in the JWT token: ```cs // This is a new Alba extension that can "stub" out // JWT token authentication var jwtSecurityStub = new JwtSecurityStub() .With("foo", "bar") .With(JwtRegisteredClaimNames.Email, "guy@company.com"); // We're calling your real web service's configuration theHost = await AlbaHost.For(jwtSecurityStub); ``` snippet source | anchor The `JwtSecurityStub` reaches into your application's configuration to find the security signing key for JWT tokens, and uses that key to sign the keys that it generates. The `JwtSecurityStub` will also disable any callouts to validate the JWT tokens with a real Open Id Connect server **so you can test your service without an external token identity server running.** The `JwtSecurityStub` will also honor the `WithClaim()` method to add additional claims on a scenario by scenario basis as shown in the previous section. ## Override a specific scheme Both `AuthenticationSecurityStub` and `JwtSecuritySnub` will replace all authentication schemes by default. If you only want a single scheme to be replaced, you can pass the scheme name via the constructor: ```cs // Stub out an individual scheme var securityStub = new AuthenticationStub("custom") .With("foo", "bar") .With(JwtRegisteredClaimNames.Email, "guy@company.com") .WithName("jeremy"); await using var host = await AlbaHost.For() .WithExtension(securityStub); ``` snippet source | anchor ## Integration with JWT Authentication ::: tip All of these extensions depend on the `JwtBearerOptions` configuration from your application. The extensions will use the `Authority` property of `JwtBearerOptions` for the Url of the OIDC token server. ::: If you want to test your ASP.NET Core web services that are authenticated by an [Open Id Connect](https://openid.net/connect/) workflow **and** you also want to be testing through the authentication from the real OIDC identity server, Alba comes with extensions to automatically fetch and apply JWT tokens to scenario tests. To use the OIDC [Client Credentials workflow](https://auth0.com/docs/flows/client-credentials-flow), you can use the `OpenConnectClientCredentials` extension: ```cs oidc = new OpenConnectClientCredentials { // These three properties are mandatory, and // would refer to matching configuration in your // OIDC server ClientId = Config.ClientId, ClientSecret = Config.ClientSecret, Scope = Config.ApiScope }; theHost = await AlbaHost.For(x => { x.ConfigureServices((ctx, collection) => collection.Configure(JwtBearerDefaults.AuthenticationScheme, options => { options.Authority = _fixture.IdentityServer.BaseAddress.ToString(); options.BackchannelHttpHandler = _fixture.IdentityServer.CreateHandler(); options.RequireHttpsMetadata = false; })); }, oidc); ``` snippet source | anchor To use the OIDC [Resource Owner Password Grant](https://docs.identityserver.io/en/release/quickstarts/2_resource_owner_passwords.html) workflow, you can use the `OpenConnectUserPassword` extension: ```cs oidc = new OpenConnectUserPassword { // All of these properties are mandatory ClientId = Config.ClientId, ClientSecret = Config.ClientSecret, UserName = "alice", Password = "alice" }; theHost = await AlbaHost.For(x => { x.ConfigureServices((ctx, collection) => collection.Configure(JwtBearerDefaults.AuthenticationScheme, options => { options.Authority = _fixture.IdentityServer.BaseAddress.ToString(); options.BackchannelHttpHandler = _fixture.IdentityServer.CreateHandler(); options.RequireHttpsMetadata = false; })); }, oidc); ``` snippet source | anchor With the `OpenConnectUserPassword` extension, you can also use a different user name and password for a single scenario with the `Scenario.UserAndPasswordIs(user, password)` extension method as shown below: ```cs [Fact] public async Task post_to_a_secured_endpoint_with_jwt_with_overridden_user_and_password() { // Building the input body var input = new Numbers { Values = new[] {2, 3, 4} }; var response = await theHost.Scenario(x => { // Alba deals with Json serialization for us x.Post.Json(input).ToUrl("/math"); // Override the user credentials for just this scenario x.UserAndPasswordIs("bob", "bob"); // Enforce that the HTTP Status Code is 200 Ok x.StatusCodeShouldBeOk(); }); var output = response.ReadAsJson(); output.Sum.ShouldBe(9); output.Product.ShouldBe(24); var user = response.Context.User; user.FindFirst("name").Value.ShouldBe("Bob Smith"); } ``` snippet source | anchor ## Windows Authentication To add support for windows authentication in your Alba specifications, we recommend checking out the [AspNetCore.TestHost.WindowsAuth](https://github.com/IntelliTect/AspNetCore.TestHost.WindowsAuth) project. --- --- url: https://jasperfx.github.io/alba/guide/endpoints.md description: >- Find ASP.NET Core endpoints that no test reaches with EndpointCoverage, and list endpoints that allow anonymous access to catch missing authorization. --- # Endpoint Coverage and Authorization Alba can report which of the application's endpoints the test suite reaches, and which ones a request without credentials can hit. Both checks describe endpoints by their HTTP methods and route pattern, such as `"GET /accounts/{id:int}"`. ## Endpoints that no test reaches The `EndpointCoverage` extension, in the `Alba.Extensions` namespace, records each endpoint that ASP.NET Core routes a request to. Pass one instance to every host of the application, then call `NotExercised()` after the last test has run to list the endpoints that no request reached: ```cs public class ApiCoverage : IAsyncLifetime { // One instance for every host of the application, however many fixtures or pools create them public static readonly EndpointCoverage Coverage = new(); public ValueTask InitializeAsync() => ValueTask.CompletedTask; public ValueTask DisposeAsync() { // As an assembly fixture, this runs after the last test in the assembly. // The exception fails the test run, naming each endpoint that no test reached. Coverage.NotExercised().ShouldBeEmpty(); return ValueTask.CompletedTask; } } public class ApiFixture : IAsyncLifetime { public IAlbaHost Host = null!; public async ValueTask InitializeAsync() { Host = await AlbaHost.For(ApiCoverage.Coverage); } public async ValueTask DisposeAsync() { await Host.DisposeAsync(); } } ``` snippet source | anchor Register the coverage class as an assembly fixture, so that its `DisposeAsync` runs after every test in the assembly: ```cs [assembly: AssemblyFixture] ``` The exception from `ShouldBeEmpty()` fails the test run. To report the gaps without failing the run, write them to the test output instead. With NUnit, check the coverage in a `[OneTimeTearDown]` method of a `[SetUpFixture]`; with TUnit, check it in an `[After(TestSession)]` hook. * One instance can be shared by any number of hosts, such as every host of an [`AlbaHostPool`](pooling.md), and it keeps its results after the hosts are disposed. Hosts that are not given the instance are not counted. * An endpoint counts as soon as routing selects it, whatever the response, so a test that expects a 401 from an existing endpoint covers it. Requests to urls that match no endpoint do not count. * Every request through the host counts: scenarios, server-sent event streams, and clients from `host.Server.CreateClient()`. * Coverage is recorded per test process. Each test project and each target framework runs in its own process and reports on its own. ## Endpoints that allow anonymous requests `EndpointsAllowingAnonymous()` lists the endpoints that a request without credentials can reach. These are the endpoints with `[AllowAnonymous]` or `AllowAnonymous()`. When the application has no fallback authorization policy, they also include every endpoint with no authorization requirement. The rules match ASP.NET Core's authorization middleware: `[AllowAnonymous]` wins over `[Authorize]`, and a fallback policy protects every endpoint that has no authorization metadata. Asserting on the list makes a cheap security regression test. A new endpoint that is missing `[Authorize]` or `RequireAuthorization()` changes the list and fails the test: ```cs [Fact] public async Task no_endpoint_allows_anonymous_requests() { await using var host = await AlbaHost.For(); // A new endpoint that forgets [Authorize] or RequireAuthorization() fails this test host.EndpointsAllowingAnonymous().ShouldBeEmpty(); } ``` snippet source | anchor For an application with public endpoints, assert the exact list instead, such as `ShouldBe(["GET /health", "POST /login"])`. Only endpoint metadata is examined. This check cannot see authorization that MVC applies through a global `AuthorizeFilter`, or that the application performs in its own code. --- --- url: https://jasperfx.github.io/alba/guide/snapshot.md description: >- Snapshot test Alba scenario results with Verify and Verify.AspNetCore to capture the full HTTP request and response. --- # Snapshot Testing Although Alba does not ship built-in snapshotting support, it does easily integrate with the popular [Verify](https://github.com/VerifyTests/Verify) framework. Given Verify and [Verify.AspNetCore](https://github.com/VerifyTests/Verify.AspNetCore) has been added and initialized for your test framework of choice, you can pass the `scenario` to `Verify()`, and response data will be captured for a complete picture of your network request. Given a test with the shape of: ```cs await using var host = await AlbaHost.For(); var scenario = await host.Scenario(s => { s.Post.Json(new MyEntity(Guid.NewGuid(), "SomeValue")).ToUrl("/json"); }); await Verify(scenario); ``` snippet source | anchor Will result in a snapshot of: ```json { Context: { Request: { Headers: { Accept: application/json, Content-Length: 67, Content-Type: application/json, Host: localhost } }, Response: { StatusCode: OK, Headers: { Content-Type: application/json; charset=utf-8 }, Value: { id: Guid_1, myValue: SomeValue } } } } ``` --- --- url: https://jasperfx.github.io/alba/guide/opentelemetry.md description: >- Trace Alba scenarios with OpenTelemetry and see integration test requests in Datadog CI Visibility or other OpenTelemetry tools. --- # Tracing & Open Telemetry Performing distributed tracing within your CI pipline is a relatively new yet powerful concept that can improve your teams response to broken, flaky or slow-performing tests. Alba has support for Open Telemetry tracing within `Scenario` calls, permitting tracing within your test pipeline with any compatible OpenTelemetry integration. An [`AlbaHostPool`](pooling.md#tracing-the-pool) also traces renting, starting and resetting its hosts. If you believe there's value in tracing additional areas of Alba, please let us know! ## Automated Instrumentation ### Datadog CI Visibility ![Datadog Tracing](/tracing.png) Datadog's CI Visibility feature is compatible with Alba, however you must be using DD .NET Tracer 2.24+ and have `DD_TRACE_OTEL_ENABLED` set to `true`. See the [documentation](https://docs.datadoghq.com/continuous_integration/tests/dotnet/) for setup information. ## Manual Instrumentation ### xUnit Manually instrumenting your tests requires a moderate amount of supporting code to work correctly. See this [repository](https://github.com/martinjt/unittest-with-otel) and related [guide](https://www.honeycomb.io/blog/monitoring-unit-tests-opentelemetry) by the team at Honeycomb.io as a starting point. --- --- url: https://jasperfx.github.io/alba/guide/bootstrapping.md description: >- Bootstrap Alba from an IHostBuilder or with StartAlbaAsync to test Startup.cs-style ASP.NET Core applications, including on .NET 11. --- # Alternative Bootstrapping Methods Whilst `AlbaHost.For` is the preferred way of bootstrapping, other options exist in Alba to cover some older & alternative scenarios. ## Initializing AlbaHost with IHostBuilder Directly To bootstrap a **ASP.NET Core Startup.cs-style** application, create a `AlbaHost` using the definition of your `IHostBuilder` as shown below. On .NET 11 this is the way to test a `Startup.cs`-style application, because `AlbaHost.For` only supports applications built with `WebApplicationBuilder` there. ```cs [Fact] public async Task build_host_from_Program() { // Bootstrap your application just as your real application does var hostBuilder = Program.CreateHostBuilder(Array.Empty()); await using var host = await AlbaHost.For(hostBuilder); // Just as a sample, I'll run a scenario against // a "hello, world" application's root url await host.Scenario(s => { s.Get.Url("/"); s.ContentShouldBe("Hello, World!"); }); } ``` snippet source | anchor ::: tip Bootstrapping an `AlbaHost` is asynchronous. Use your test framework's asynchronous initialization support (e.g. xUnit's `IAsyncLifetime`) to start the host. ::: Or alternatively, you can use one of the Alba extension methods off of `IHostBuilder` to start an `AlbaHost` object in a fluent interface style: ```cs [Fact] public async Task fluent_interface_bootstrapping() { await using var host = await Program .CreateHostBuilder(Array.Empty()) .StartAlbaAsync(); // Just as a sample, I'll run a scenario against // a "hello, world" application's root url await host.Scenario(s => { s.Get.Url("/"); s.ContentShouldBe("Hello, World!"); }); } ``` snippet source | anchor The `AlbaHost` is an extension of the standard .Net Core [IHost](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihost) interface with a few additions for testing support. While you can always access the underlying `TestServer` through the `IAlbaHost.Server` property, you're mostly going to be using the `Scenario()` method to write Alba "Scenario" tests. ::: tip Every bootstrapping method returns an `AlbaHostBuilder`, so configuration values, service registrations, and extensions can be chained on before the host is awaited. See [Configuring the Host Fluently](gettingstarted.md#configuring-the-host-fluently). ::: --- --- url: https://jasperfx.github.io/alba/guide/history.md description: >- How Alba grew from FubuMVC's scenario testing into a wrapper around the ASP.NET Core TestServer, and how AlbaHost is put together. --- # History & Architecture What is now Alba was originally created in the early 2010's as a mechanism to test the content negotiation features in [FubuMVC](https://fubumvc.github.io), an alternative web application framework that predates ASP.NET Core. We needed to run the entire web handling stack including all the middleware and HTTP endpoints just as the application would be configured. The [early *scenario* support in FubuMVC](https://jeremydmiller.com/2015/11/05/testing-http-handlers-with-no-web-server-in-sight/) was a way to run HTTP requests end to end completely in memory and make declarative checks on expected HTTP behavior. As FubuMVC wound down as a project, the scenario testing code was first extracted out into its own library called *Alba*, then ported to depend on an [OWIN-based](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/owin) execution pipeline. And then again with the advent of ASP.NET Core and the deprecation of OWIN, Alba was again re-wired to use the newer HTTP abstractions from the ASP.NET Core [HttpContext](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext) type. At this point, Alba is a value added wrapper around the [ASP.NET Core TestServer](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests). The *scenario* testing in Alba was inspired by the [testing support in the Scala PlayFramework](https://www.playframework.com/documentation/2.8.x/ScalaFunctionalTestingWithSpecs2). When using Alba, you bootstrap your web application in memory through one of three models: the application's `IHostBuilder`, a `WebApplicationBuilder`, or [WebApplicationFactory](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1) for an application's entry point. Each returns an awaitable `AlbaHostBuilder` that starts the application and hands back Alba's `AlbaHost`, which will be the entry point to using Alba in all of your integration tests. `AlbaHost` is shown in the class diagram below: ![AlbaHost Class Diagram](/ClassDiagram.drawio.png) `AlbaHost` implements the ASP.NET Core [IHost](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihost) interface for convenience and familiarity. Internally it is tracking the actual `IHost` for your application running in memory as well as an instance of the ASP.NET Core [TestServer](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests) that will actually be used to execute HTTP requests against the application in memory. --- --- url: https://jasperfx.github.io/alba/scenarios/writingscenarios.md description: >- Write declarative Alba scenarios that send an HTTP request through your ASP.NET Core application in memory and assert on the response. --- # Writing Scenarios ::: tip Alba executes requests through your application without any knowledge of the middleware, controllers, or the other mechanisms that may be handling the request in your application. ::: For the purpose of this sample, let's say you have a very simple web service application with the following controller endpoint: ```cs public enum OperationType { Add, Subtract, Multiply, Divide } public class OperationRequest { public OperationType Type { get; set; } public int One { get; set; } public int Two { get; set; } } public class OperationResult { public int Answer { get; set; } public string Method { get; set; } } [ApiController] [Route("[controller]")] public class MathController : Controller { [HttpGet("add/{one}/{two}")] public OperationResult Add(int one, int two) { return new OperationResult { Answer = one + two }; } [HttpPut] public OperationResult Put([FromBody]OperationRequest request) { switch (request.Type) { case OperationType.Add: return new OperationResult{Answer = request.One + request.Two, Method = "PUT"}; case OperationType.Multiply: return new OperationResult{Answer = request.One * request.Two, Method = "PUT"}; case OperationType.Subtract: return new OperationResult{Answer = request.One - request.Two, Method = "PUT"}; default: throw new ArgumentOutOfRangeException(nameof(request.Type)); } } [HttpPost] public OperationResult Post([FromBody]OperationRequest request) { switch (request.Type) { case OperationType.Add: return new OperationResult{Answer = request.One + request.Two, Method = "POST"}; case OperationType.Multiply: return new OperationResult{Answer = request.One * request.Two, Method = "POST"}; case OperationType.Subtract: return new OperationResult{Answer = request.One - request.Two, Method = "POST"}; default: throw new ArgumentOutOfRangeException(nameof(request.Type)); } } ``` snippet source | anchor Back in your test project, the easiest, and probably most common, usage of Alba is to send and verify JSON message bodies to `Controller` actions. To that end, let's test the GET method in that controller above by passing a url and verifying the results: ```cs [Fact] public async Task get_happy_path() { await using var system = await AlbaHost.For(); // Issue a request, and check the results var result = await system.GetAsJson("/math/add/3/4"); result.Answer.ShouldBe(7); } ``` snippet source | anchor So what just happened in that test? First off, the call to `AlbaHost.For(IHostBuilder)` bootstraps your web application. The call to `host.GetAsJson("/math/add/3/4")` is performing these steps internally: 1. Formulate an [HttpRequest](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest) object that will be passed to the application 2. Executes the web request against your application 3. Assert in this simple use case that the response status code is `200 OK` 4. Read the raw JSON coming off the [HttpResponse](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpresponse) 5. Deserialize the raw JSON to the requested `OperationResult` type using the Json formatter of the running application 6. Returns the resulting `OperationResult` Alright then, let's try posting JSON in and examining the JSON out: ```cs [Fact] public async Task post_and_expect_response() { await using var system = await AlbaHost.For(); var request = new OperationRequest { Type = OperationType.Multiply, One = 3, Two = 4 }; var result = await system.PostJson(request, "/math") .Receive(); result.Answer.ShouldBe(12); result.Method.ShouldBe("POST"); } ``` snippet source | anchor It's a little more complicated, but the same goal is realized here. Allow the test author to work in terms of the application model objects while still exercising the entire HTTP middleware stack. Don't stop here though, Alba also gives you the ability to declaratively assert on elements of the `HttpResponse` like expected header values, status codes, and assertions against the response body. In addition, Alba provides a lot of helper facilities to work with the raw `HttpResponse` data. ::: tip The scenario support is not hard coded to use Newtonsoft.Json for Json serialization and will instead use the configured Json formatters within your application. Long story short, Alba now supports applications using System.Text.Json as well as Newtonsoft.Json. ::: --- --- url: https://jasperfx.github.io/alba/scenarios/urls.md description: >- Set the request url, HTTP method, query string and route name values in Alba scenarios. --- # Working with Urls The simplest way to specify the url for the request is to use one of these calls shown below, depending upon the HTTP method: ```cs public async Task specify_url(AlbaHost system) { await system.Scenario(_ => { // Directly specify the Url against a given // HTTP method _.Get.Url("/"); _.Put.Url("/"); _.Post.Url("/"); _.Delete.Url("/"); _.Patch.Url("/"); _.Head.Url("/"); _.Query.Url("/"); _.Options.Url("/"); }); } ``` snippet source | anchor ## Query string parameters Query string parameters can be appended individually or from the public properties and fields of an object: ```cs public async Task query_string_parameters(AlbaHost system) { await system.Scenario(_ => { // Add individual query string parameters _.Get.Url("/search").QueryString("q", "alba").QueryString("page", "2"); // Or append one parameter per public property or field // of an object _.Get.Url("/search").QueryString(new { q = "alba", page = 2 }); }); } ``` snippet source | anchor ## Route names Instead of repeating a url that the endpoint already owns, a scenario can address an endpoint by its ASP.NET Core route name. Alba generates the url from the running application's routing, so the test keeps working when the route pattern changes: ```cs public async Task route_names(IAlbaHost system) { var accountId = 42; await system.Scenario(_ => { // Route values fill the route parameters of the named endpoint, // and the rest become the query string _.Get.RouteNamed("GetAccount", new { accountId, include = "deposits" }); }); await system.Scenario(_ => { // With a request body, name the route after the body _.Post.Json(new { Amount = 100m }).ToRouteNamed("MakeDeposit", new { accountId }); }); } ``` snippet source | anchor Route names come from minimal API `.WithName(...)`, MVC attribute routes with `Name = ...`, and Wolverine's `RouteName` endpoint property. Route values can be an anonymous object, a dictionary, or a `RouteValueDictionary`. Values that are not route parameters become the query string, and `QueryString(...)` can add more after the url is generated. Route name lookup ignores case. An unknown route name, a missing required route value, or a value that fails a route constraint throws an `InvalidOperationException` before the request is sent. The message lists the route names the application knows, or the route pattern and the values that are missing, instead of letting the scenario fail with a 404. --- --- url: https://jasperfx.github.io/alba/scenarios/statuscode.md description: >- Assert on the HTTP status code of the response in Alba scenarios, or skip the default 2xx check. --- # Http Status Codes You can declaratively check the status code with this syntax: ```cs public async Task check_the_status(IAlbaHost system) { await system.Scenario(_ => { // Shorthand for saying that the StatusCode should be 200 _.StatusCodeShouldBeOk(); // Or a specific status code _.StatusCodeShouldBe(403); // Any status code between 200 and 299; this is the // default expectation for every scenario _.StatusCodeShouldBeSuccess(); // Ignore the status code altogether _.IgnoreStatusCode(); }); } ``` snippet source | anchor Do note that by default, if you do not specify the expected status code, Alba assumes that the request should return a success status code (200-299) and will fail the scenario otherwise. Use `StatusCodeShouldBe(...)` or `StatusCodeShouldBeOk()` to require one exact status code, or `Scenario.IgnoreStatusCode()` to skip the check altogether. `StatusCodeShouldBeSuccess()` restores the default success-range expectation after either of those. --- --- url: https://jasperfx.github.io/alba/scenarios/headers.md description: >- Set request headers and assert on response header values in Alba scenarios for ASP.NET Core. --- # Working with Headers We really didn't worry much about HTTP niceties when I started web programming in the late 90's, but you can no longer get away with that in today's world. To more correctly use HTTP, Alba comes with some helpers to deal with HTTP header values. ## Setting Request Headers To set request headers, you can directly write against the `HttpContext.Request.Headers` collection: ```cs public async Task setting_request_headers(IAlbaHost system) { await system.Scenario(_ => { _.WithRequestHeader("foo", "bar"); }); } ``` snippet source | anchor There are also some specific helpers for very common [content negotiation-related](https://en.wikipedia.org/wiki/Content_negotiation) headers: ```cs public async Task conneg_helpers(IAlbaHost system) { await system.Scenario(_ => { // Set the accepts header on the request _.Get.Url("/").Accepts("text/plain"); // Specify the etag header value _.Get.Url("/").Etag("12345"); // Set the content-type header on the request _.Post.Url("/").ContentType("text/json"); // This is a superset of the code above that // will set the content-type header as well _.Post.Json(new InputModel()).ToUrl("/"); }); } ``` snippet source | anchor **Do add extension methods off of the Alba `Scenario` class for common operations in your tests to remove some of the tedium.** ## Asserting on Expected Response Headers Alba comes with some out of the box assertions to declaratively check expected header values: ```cs public async Task asserting_on_header_values(IAlbaHost system) { await system.Scenario(_ => { // Assert that there is one and only one value equal to "150" _.Header(HeaderNames.ContentLength).SingleValueShouldEqual("150"); // Assert that there is no value for this response header _.Header(HeaderNames.Connection).ShouldNotBeWritten(); // Only write one value for this header _.Header(HeaderNames.SetCookie).ShouldHaveOneNonNullValue(); // Assert that the header has any values _.Header(HeaderNames.ETag).ShouldHaveValues(); // Assert that the header has the given values _.Header(HeaderNames.WWWAuthenticate).ShouldHaveValues("NTLM", "Negotiate"); // Assert that the header matches a regular expression _.Header(HeaderNames.Location).SingleValueShouldMatch(new Regex(@"^/items/\d*$")); // Check the content-type header _.ContentTypeShouldBe("text/json"); }); } ``` snippet source | anchor You do also have the ability to just interrogate the `HttpContext.Response` in your unit test methods for anything not covered in the helpers above. --- --- url: https://jasperfx.github.io/alba/scenarios/json.md description: >- Send JSON request bodies, assert on JSON responses with JsonPath, and deserialize results with your application's own System.Text.Json or Newtonsoft.Json settings. --- # Sending and Checking Json JSON serialization is done with the configured input and output formatters within the underlying application. This means that Alba can support systems using both System.Text.Json and Newtonsoft.Json. Alba picks the application's System.Text.Json or Newtonsoft.Json formatters even when other registered formatters (such as ASP.NET Core OData's) also advertise `application/json`, so the JSON helpers work in applications that mix OData and plain API controllers. ## Sending Json ::: tip Minimal API and MVC Core endpoints do JSON serialization a little bit differently. When testing a system that has mixed Minimal API and MVC Core endpoints **and the JSON serialization has been customized within the application**, you may need to help Alba out a little bit to know what type of route you're testing. ::: Since posting Json to a web server API is so common, Alba has some helpers for writing Json to the request: ```cs public async Task send_json(IAlbaHost host) { await host.Scenario(_ => { // This serializes the Input object to json, // writes it to the HttpRequest.Body, and sets // the accepts & content-type header values to // application/json _.Post .Json(new Input {Name = "Max", Age = 13}) .ToUrl("/person"); }); } public async Task send_json_minimal_api(IAlbaHost host) { await host.Scenario(_ => { // In a system that has mixed Minimal API and MVC usage, // you may need to help Alba know if the route being tested // should use Minimal API or MVC Core compliant JSON testing _.Post .Json(new Input {Name = "Max", Age = 13}, JsonStyle.MinimalApi) .ToUrl("/person"); }); } ``` snippet source | anchor ## Sending Raw Json When you already have the JSON payload as a string — malformed-input tests, captured payloads, and the like — skip serialization and write it directly: ```cs await host.Scenario(_ => { _.Post.RawJson("{\"name\": \"Max\", \"age\": 13}").ToUrl("/person"); }); ``` This writes the string to the request body verbatim and sets the `content-type` header to `application/json`. ## Reading Json To deserialize the response body with Json to interrogate the results in a strong typed way, use this syntax: ```cs public async Task read_json(IAlbaHost host) { var result = await host.Scenario(_ => { _.Get.Url("/output"); }); // This deserializes the response body to the // designated Output type var output = result.ReadAsJsonAsync(); // do assertions against the Output model } ``` snippet source | anchor You can also use a shorthand syntax to skip `Scenario()` like this: ```cs public async Task read_json_shorthand(IAlbaHost host) { var output = await host.GetAsJson("/output"); // do assertions against the Output model } ``` snippet source | anchor This code snippet is functionally identical to the previous usage. ## Asserting on JSON Structure `JsonPath(...)` asserts on one value in a JSON response body, without using a DTO. Expected values are serialized with the application's own JSON settings, so enums, naming policies and converters match what the application writes: ```cs [Fact] public async Task values_at_paths() { await _host.Scenario(x => { x.Get.Url("/order"); x.JsonPath("$.id").ShouldBe(7); x.JsonPath("$.customer.name").ShouldBe("Alba"); x.JsonPath("$.lines[1].sku").ShouldBe("B-2"); x.JsonPath("$['customer']['name']").ShouldBe("Alba"); // The leading "$." is optional x.JsonPath("lines[0].quantity").ShouldBe(2); }); } ``` snippet source | anchor Paths use a small JSONPath subset: `$` for the root, `.name` or `['name']` for a property, and `[0]` for an array item. The leading `$.` can be left out. Filters, wildcards and recursive descent are not supported. `ShouldBe(...)` compares structurally: property order and whitespace do not matter, and numbers compare by value, so `150` equals `150.0`. Objects, anonymous types and collections compare the same way, and the path `$` compares the whole body: ```cs [Fact] public async Task objects_compare_structurally_regardless_of_property_order() { await _host.Scenario(x => { x.Get.Url("/order"); x.JsonPath("$.lines[0]").ShouldBe(new { quantity = 2, sku = "A-1" }); x.JsonPath("$.customer").ShouldBe(new { email = (string)null, name = "Alba" }); }); } ``` snippet source | anchor `ShouldExist()`, `ShouldNotExist()` and `ShouldHaveCount(...)` check that a member is present or absent, or that an array has a given length. A member whose value is JSON `null` counts as present: ```cs [Fact] public async Task existence_and_counts() { await _host.Scenario(x => { x.Get.Url("/order"); x.JsonPath("$.customer.email").ShouldExist().ShouldBe(null); x.JsonPath("$.customer.phone").ShouldNotExist(); x.JsonPath("$.lines[2]").ShouldNotExist(); x.JsonPath("$.lines").ShouldHaveCount(2); }); } ``` snippet source | anchor As with the other scenario assertions, every failed path is reported together with the response body. For error responses in the problem details format, see [Problem Details](problemdetails.md). ## Shortcut for Pure Json Services If you don't care about any additional HTTP headers or need to verify any HTTP status code except for `200 Ok`, you can use this shorthand syntax to quickly post and receive results from a web service: ```cs [Fact] public async Task post_and_expect_response() { await using var system = await AlbaHost.For(); var request = new OperationRequest { Type = OperationType.Multiply, One = 3, Two = 4 }; var result = await system.PostJson(request, "/math") .Receive(); result.Answer.ShouldBe(12); result.Method.ShouldBe("POST"); } ``` snippet source | anchor There are similar helpers for other HTTP verbs like `PUT` and `DELETE`. --- --- url: https://jasperfx.github.io/alba/scenarios/problemdetails.md description: >- Assert on RFC 9457 problem details and validation problem responses from ASP.NET Core in Alba scenarios. --- # Problem Details ASP.NET Core writes error responses in the [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details format from `Results.Problem()`, `Results.ValidationProblem()`, `[ApiController]` model validation and the exception handler, and frameworks such as Wolverine do the same. Your application might too. Alba has assertions for these responses: ```cs [Fact] public async Task problem_details_members() { await _host.Scenario(x => { x.Post.Url("/withdrawals"); x.ShouldReturnProblemDetails(409) .WithTitle("Insufficient funds") .WithDetail("The balance is 10.00") .WithType("https://example.com/problems/insufficient-funds") .WithExtension("balance", 10m) .WithExtension("limits", new[] { 5, 50 }); }); } [Fact] public async Task validation_errors_by_field() { await _host.Scenario(x => { x.Post.Url("/deposits"); x.ShouldReturnValidationProblem() .WithValidationError("Amount", "must be positive") .WithValidationError("amount", "two decimal places") .WithValidationError("Currency"); }); } ``` snippet source | anchor * `ShouldReturnProblemDetails(statusCode)` asserts the status code and the `application/problem+json` content type. * `ShouldReturnValidationProblem()` also requires an `errors` member. The status code defaults to 400; pass another one, such as 422, if the application uses it. * `WithTitle(...)`, `WithDetail(...)` and `WithType(...)` compare those members exactly. * `WithExtension(name, value)` compares an extension member structurally, serializing the expected value with the application's JSON settings in the same way as [`JsonPath(...)`](json.md#asserting-on-json-structure). * `WithValidationError(field)` asserts that a field has a validation error. Field names are matched without regard to case, because applications differ in how they case the keys. Pass a second argument to require that one of the field's messages contains that text. All failures are reported together with the response body. --- --- url: https://jasperfx.github.io/alba/scenarios/text.md description: >- Send plain text request bodies and read or assert on plain text responses in Alba scenarios. --- # Working with Plain Text Requests If you find yourself needing to test HTTP endpoints that either send text or return text, Alba has you covered with some built in helpers. ## Reading the Response Text To read the response body as text, use this syntax: ```cs public async Task read_text(IAlbaHost host) { var result = await host.Scenario(_ => { _.Get.Url("/output"); }); // This deserializes the response body to the // designated Output type var outputString = await result.ReadAsTextAsync(); // do assertions against the Output string } ``` snippet source | anchor ## Assertions against the Response Text You have these built in operations for asserting on the response body text: ```cs public async Task assert_on_content(IAlbaHost host) { await host.Scenario(_ => { _.ContentShouldBe("exactly this"); _.ContentShouldContain("some snippet"); _.ContentShouldNotContain("some warning"); }); } ``` snippet source | anchor ## Sending Text Lastly, you can send text to an HTTP endpoint with this syntax: ```cs public async Task send_text(IAlbaHost host) { await host.Scenario(_ => { _.Post.Text("some text").ToUrl("/textdata"); }); } ``` snippet source | anchor Do note that this also sets the `content-length` header to the string length and sets the `content-type` header of the request to "text/plain." ## Sending Binary Data Raw bytes or a stream can be written directly to the request body: ```cs await host.Scenario(_ => { _.Post.ByteArray(bytes).ToUrl("/upload").ContentType("application/octet-stream"); // or from any readable Stream _.Post.Stream(stream).ToUrl("/upload").ContentType("application/octet-stream"); }); ``` Both set the `content-length` header from the written content; set the `content-type` header yourself as shown. --- --- url: https://jasperfx.github.io/alba/scenarios/sse.md description: >- Test ASP.NET Core server-sent event endpoints with Alba, reading finite streams from a scenario or consuming live streams. --- # Server-Sent Events Alba can test [server-sent event](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) endpoints two ways: finite streams through an ordinary scenario, and live streams through a dedicated streaming API. Events are surfaced as `SseItem` from `System.Net.ServerSentEvents`. ## Finite Streams If the endpoint writes a bounded number of events and completes, a regular scenario works — the response is fully buffered like any other. Parse the buffered body with `ReadAsServerSentEvents()`: ```cs public async Task read_a_finite_event_stream(IAlbaHost host) { // The endpoint writes a bounded number of events and completes, // so an ordinary scenario buffers the whole response var result = await host.Scenario(x => { x.Get.Url("/sse/finite"); }); // Parse the buffered body as server-sent events var events = result.ReadAsServerSentEvents(); // Or deserialize each data payload with the application's // JSON options var typed = result.ReadAsServerSentEvents(); } ``` snippet source | anchor The typed overload `ReadAsServerSentEvents()` deserializes each `data:` payload as JSON using the application's own `JsonSerializerOptions`, exactly like `ReadAsJson()` does. Both reads are repeatable. ## Live Streams Endpoints that stream indefinitely (or that you want to observe mid-stream) can't be buffered. Use `StreamServerSentEvents` instead of `Scenario`: ```cs public async Task stream_live_events(IAlbaHost host) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // Returns as soon as the response headers arrive; // the response body is never buffered await using var stream = await host.StreamServerSentEvents(x => { x.Get.Url("/sse/infinite"); }, timeout.Token); // Events are yielded as the application writes them await foreach (var item in stream.ReadEvents(timeout.Token)) { if (item.Data == "done") break; } // Disposing the stream aborts the request, cancelling the // endpoint's HttpContext.RequestAborted token } ``` snippet source | anchor The call returns as soon as the application flushes its response headers, and `ReadEvents()` / `ReadEvents()` yield each event as the application writes it. The event stream can only be consumed once. Disposing the `SseStreamResult` aborts the in-flight request, which cancels the endpoint's `HttpContext.RequestAborted` token — a well-behaved streaming endpoint will observe that and stop. Any `AfterEach`/`AfterEachAsync` actions run at disposal with a `null` `HttpContext`. All `BeforeEach`/`BeforeEachAsync` actions and Alba's security extensions (such as `JwtSecurityStub`, including per-scenario claims via `WithClaim`) apply to streams exactly as they do to scenarios. ## Limitations * Response assertions (`ContentShouldContain`, header assertions, custom assertions) are not supported with `StreamServerSentEvents` and are rejected up front — assert on the streamed events instead. The expected status code *is* honored: a mismatch (for example a 401 from a missed auth setup) throws with the actual response body instead of presenting an empty stream. * The response must have the `text/event-stream` content type. * The `CancellationToken` argument to `StreamServerSentEvents` is the escape hatch for an endpoint that never starts its response. --- --- url: https://jasperfx.github.io/alba/scenarios/xml.md description: >- Send XML request bodies and read XML responses in Alba scenarios using XmlSerializer. --- # Sending and Checking Xml I wasn't sure that Xml over HTTP was all that common of a use case anymore, but the very first project we used Alba on turned out to be an Xml API, so here we are. In all cases, Alba just uses the old, built in `XmlSerializer` from the .Net BCL. ## Sending Xml in the Request There's a similar helper for Xml serialization that allows you to send an object as xml to the web request: ```cs public async Task send_xml(IAlbaHost host) { await host.Scenario(_ => { // This serializes the Input object to xml, // writes it to the HttpRequest.Body, and sets // the accepts & content-type header values to // application/xml _.Post .Xml(new Input {Name = "Max", Age = 13}) .ToUrl("/person"); // OR, if url lookup is enabled, this is an equivalent: _.Post.Xml(new Input {Name = "Max", Age = 13}); }); } ``` snippet source | anchor ## Reading Xml from the Response There's a helper off of the `HttpResponseBody` for reading Xml from the response: ```cs public async Task read_xml(IAlbaHost host) { var result = await host.Scenario(_ => { _.Get.Url("/output"); }); // This deserializes the response body to the // designated Output type var output = result.ReadAsXml(); // do assertions against the Output model // OR, if you just want the XmlDocument itself: XmlDocument document = await result.ReadAsXmlAsync(); } ``` snippet source | anchor --- --- url: https://jasperfx.github.io/alba/scenarios/formdata.md description: >- Post HTML form data and multipart file uploads to ASP.NET Core endpoints in Alba scenarios. --- # Sending Form Data ## Standard Form Data Posting HTTP form data in the request can be done with the extension method shown below: ```cs public async Task write_form_data(IAlbaHost system) { var form1 = new Dictionary { ["a"] = "what?", ["b"] = "now?", ["c"] = "really?" }; await system.Scenario(_ => { // This writes the dictionary values to the HTTP // request as form data, and sets the content-length // header as well as setting the content-type // header to application/x-www-form-urlencoded _.WriteFormData(form1); }); } ``` snippet source | anchor There's a second overload that attempts to use an object and its properties to populate the form data: ```cs [Fact] public async Task can_bind_to_form_data() { await using var system = await AlbaHost.For(); var input = new InputModel { One = "one", Two = "two", Three = "three" }; await system.Scenario(_ => { _.Post.FormData(input) .ToUrl("/gateway/insert"); }); GatewayController.LastInput.ShouldNotBeNull(); GatewayController.LastInput.One.ShouldBe("one"); GatewayController.LastInput.Two.ShouldBe("two"); GatewayController.LastInput.Three.ShouldBe("three"); } ``` snippet source | anchor Do note that this only adds first level properties, so if you need to deeper accessors like add "Prop1.Prop2.Prop3," you'll have to resort to the dictionary approach. ## Multipart Form Data & File Uploads `MultipartFormData(...)` builds a multipart body of form fields and file uploads: ```cs var result = await host.Scenario(x => { x.Post.MultipartFormData(form => form // Files with the same name bind to a collection; the content type // comes from the file name extension unless one is passed .File("files", Encoding.UTF8.GetBytes("Hello there!"), "greeting.txt") .File("files", image, "TestImage.jpg") .Field("additionalContent", "My additional string of fun times")) .ToUrl("/api/files/upload"); x.StatusCodeShouldBeOk(); }); ``` snippet source | anchor * `File(name, bytes, fileName)` and `File(name, stream, fileName)` add a file. The application binds it by `name`, to an `IFormFile` parameter or property, or to a collection when several files share the name. * The file's content type comes from the extension of `fileName`, such as `text/csv` for `report.csv`, and falls back to `application/octet-stream`. Pass a content type as the last argument to override it. * A stream is read when the request is sent, so keep it open until the scenario completes. * `Field(name, value)` adds a form value. Minimal API endpoints that bind `IFormFile` validate an antiforgery token when the application uses antiforgery. Call `.DisableAntiforgery()` on such an endpoint in the test host, or send a token, because Alba does not supply one. For full control over the body, pass a `MultipartFormDataContent` instead: ```cs public async Task write_multipart_form_data(IAlbaHost system) { // Read our file into a stream await using var imageFile = File.OpenRead("TestImage.jpg"); // Extract the name from the path var imageFileName = Path.GetFileName(imageFile.Name); // Create the stream content using var content = new StreamContent(imageFile); // Remember to manually set the media type as it won't be done automatically content.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Image.Jpeg); // Create the MultiPartForm content & add the file using var formData = new MultipartFormDataContent(); formData.Add(content, "files", imageFileName); // If you have other content in the form object, you can add it as well! formData.Add(new StringContent("My additional metadata"), "metadata"); var result = await system.Scenario(_ => { // This extension will write the content to the request // body and set the required headers _.Post.MultipartFormData(formData).ToUrl("/api/files/upload"); _.StatusCodeShouldBeOk(); }); } ``` snippet source | anchor --- --- url: https://jasperfx.github.io/alba/scenarios/setup.md description: >- Run setup and teardown around every Alba scenario with BeforeEach, BeforeEachAsync and AfterEach. --- # Before and After actions Alba lets you register actions that run immediately before or after each scenario's HTTP request for common setup or teardown work like setting up authentication credentials or tracing. Registrations are additive — every registered action runs for every scenario. There are two kinds of *before* actions: * `BeforeEachAsync(Func)` — asynchronous preparation that runs **before** the HTTP request is created. It receives the `Scenario`, so it can do real asynchronous work (fetch a token, hit a database) and then modify the outgoing request through `scenario.ConfigureHttpContext(...)` or helpers like `scenario.WithRequestHeader(...)` and `scenario.WithBearerToken(...)`. * `BeforeEach(Action)` — synchronous work that runs against the live `HttpContext` immediately before the request executes. All asynchronous prepare actions run first (in registration order), then all synchronous actions (in registration order). Here's a sample: ```cs // Synchronously system.BeforeEach(context => { // Modify the HttpContext immediately before each // Scenario()/HTTP request is executed context.Request.Headers.Append("trace", "something"); }); system.AfterEach(context => { // perform an action immediately after the scenario/HTTP request // is executed }); // Asynchronously, before the HTTP request executes. Modify the // outgoing request itself through scenario.ConfigureHttpContext() system.BeforeEachAsync(scenario => { // do something asynchronous here return Task.CompletedTask; }); system.AfterEachAsync(context => { // do something asynchronous here return Task.CompletedTask; }); ``` snippet source | anchor --- --- url: https://jasperfx.github.io/alba/scenarios/assertions.md description: >- Write custom response assertions for Alba scenarios by implementing IScenarioAssertion and exposing them as extension methods. --- # Writing Custom Assertions ::: tip All of the assertions are applied during the Scenario execution, and any failures will be reported out in the Exception message thrown by Alba on Scenario failures. ::: The Scenario assertions in Alba are completely extensible and you can happily add your own via extension methods - but please send anything that's generally useful as a pull request to Alba itself ;-) The first step is to write your own implementation of this interface: ```cs public interface IScenarioAssertion { void Assert(Scenario scenario, AssertionContext context); } ``` snippet source | anchor As an example, here's the assertion from Alba that validates that the response body is supposed to ```cs internal sealed class BodyContainsAssertion : IScenarioAssertion { public string Text { get; set; } public BodyContainsAssertion(string text) { Text = text; } public void Assert(Scenario scenario, AssertionContext context) { // Context has this useful extension to read the body as a string. // This will bake the body contents into the exception message to make debugging easier. var body = context.ReadBodyAsString(); if (!body.Contains(Text)) { // Add the failure message to the exception. This exception only // gets thrown if there are failures. context.AddFailure($"Expected text '{Text}' was not found in the response body"); } } } ``` snippet source | anchor Once you have your assertion class, you can apply it to a scenario through an extension method against the `Scenario` class. Here's the `Scenario.ContentShouldContain(text)` implementation from Alba itself: ```cs /// /// Assert that the Http response contains the designated text /// /// /// /// public static Scenario ContentShouldContain(this Scenario scenario, string text) { return scenario.AssertThat(new BodyContainsAssertion(text)); } ``` snippet source | anchor Finally, use your new assertion in a Scenario like this: ```cs [Fact] public Task using_scenario_with_ContentShouldContain_declaration_happy_path() { router.Handlers["/one"] = c => { return c.Response.WriteAsync("**just the marker**"); }; return host.Scenario(x => { x.Get.Url("/one"); x.ContentShouldContain("just the marker"); }); } ``` snippet source | anchor --- --- url: https://jasperfx.github.io/alba/scenarios/redirects.md description: >- Assert that ASP.NET Core endpoints return temporary or permanent redirects to an expected url in Alba scenarios. --- # Working with Redirects ## Asserting on Expected Redirect Responses Alba comes with some out of the box assertions to declaratively check expected redirects. ```cs public async Task asserting_redirects(IAlbaHost system) { await system.Scenario(_ => { // should redirect to the url _.RedirectShouldBe("/redirect"); // should redirect permanently to the url _.RedirectPermanentShouldBe("/redirect"); }); } ``` snippet source | anchor