Skip to content

Getting Started

What is Alba?

Alba is a class library that you use in combination with unit testing tools like xUnit.Net or NUnit 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.

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 7.0+, Alba only supports .NET 6.0 or greater. You can still use older versions of Alba to test previous versions of ASP.NET Core.

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.

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 is compatible with both traditional-style Startup.cs projects as well as the new WebApplicationBuilder minimal approach. The following instructions work with both models.

As an example, consider this very small ASP.NET Core application utilizing the new Minimal API 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.Run();

public record MyEntity(Guid Id);
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.Run();

public record MyEntity(Guid Id);

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 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
  <ItemGroup>
    <InternalsVisibleTo Include="ProjectName.Tests" />
  </ItemGroup>
  <ItemGroup>
    <InternalsVisibleTo Include="ProjectName.Tests" />
  </ItemGroup>

We can now scaffold the AlbaHost using AlbaHost.For<T>, where T is your applications entry point:

cs
await using var host = await AlbaHost.For<global::Program>(x =>
{
    x.ConfigureServices((context, services) =>
    {
        services.AddSingleton<IService, ServiceA>();
    });
});
await using var host = await AlbaHost.For<global::Program>(x =>
{
    x.ConfigureServices((context, services) =>
    {
        services.AddSingleton<IService, ServiceA>();
    });
});

snippet source | anchor

The AlbaHost.For<T>(Action<WebApplicationFactory<T>> 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 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 or NUnit

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<global::Program>();
    
    // 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();
    });
}
[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<global::Program>();
    
    // 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<Scenario> 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<global::Program>();

    var guid = Guid.NewGuid();
    var res = await host.Scenario(_ =>
    {
        _.Post.Json(new MyEntity(guid)).ToUrl("/json");
        _.StatusCodeShouldBeOk();
    });

    var json = await res.ReadAsJsonAsync<MyEntity>();
    Assert.Equal(guid, json.Id);
}
[Fact]
public async Task should_return_entity_assert_response()
{
    await using var host = await AlbaHost.For<global::Program>();

    var guid = Guid.NewGuid();
    var res = await host.Scenario(_ =>
    {
        _.Post.Json(new MyEntity(guid)).ToUrl("/json");
        _.StatusCodeShouldBeOk();
    });

    var json = await res.ReadAsJsonAsync<MyEntity>();
    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<global::Program>();
    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;

}
[Fact]
public async Task should_say_hello_world_with_raw_objects()
{
    await using var host = await AlbaHost.For<global::Program>();
    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<global::Program>(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<IExternalWebService>(stubbedWebService);
        s.PostConfigure<MvcNewtonsoftJsonOptions>(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
    });
var stubbedWebService = new StubbedWebService();

await using var host = await AlbaHost.For<global::Program>(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<IExternalWebService>(stubbedWebService);
        s.PostConfigure<MvcNewtonsoftJsonOptions>(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