Fork me on GitHub

Alba 3.0.0


Next

Documentation

Previous

Alba

Getting Started


Note! As of Alba 2.0+, there is now only the main Alba library and it only supports netcoreapp2.1+ applications. We have dropped all support for any version of ASP.Net Core before 2.1. The Alba.AspNetCore2 package has been deprecated, please switch to Alba proper.

Alba is a class library that you use in combination with unit testing tools like xUnit.Net to author integration tests against ASP.Net Core HTTP endpoints that actually exercises the full application stack by running HTTP requests through your ASP.Net system in memory. As of version 2.0, Alba uses the built in TestHost internally to greatly improve its compatibility with many quirks of the ASP.Net Core model.

Note! With the advent of the Microsoft.AspNetCore.All metapackage that is part of the new official guidance, ASP.Net Core projects are very susceptible to Nuget version incompatibility issues and diamond dependency conflicts at runtime. Please see the comments in the csproj file shown below for workarounds

To start using Alba to write integration tests, make sure you have a test project for your web application and add a Nuget reference to Alba. If your application is called "WebApp," the csproj file should look like this (please note the comments for workarounds to possible Nuget issues):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netcoreapp2.1;netcoreapp2.2</TargetFrameworks>
    <DebugType>portable</DebugType>

    <!--THIS IS IMPORTANT TO PREVENT NUGET CONFLICTS BETWEEN ALBA
        AND YOUR APPLICATION  -->
    <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
  </PropertyGroup>
  <ItemGroup>
    <!-- The reference to your ASP.Net Core application project  -->
    <ProjectReference Include="..\WebApp\WebApp.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
    <PackageReference Include="xunit" Version="2.4.0" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />

    <!-- This is still important to reference explicitly even though it's
         a transitive dependency of your web application  -->
    <PackageReference Include="Microsoft.AspNetCore.All" />
  </ItemGroup>


</Project>

In addition, another workaround to potential binding conflicts is to explicitly specify the version of Microsoft.AspNetCore.All in your test project.

Writing your first specification

For the purpose of this sample, let's say you generate a new web api project with the standard dotnet new webapi template. If you do that, you'll have this bootstrapping code in your Program.Main() method:

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup&lt;Startup&gt;();
    }

In this default setup, your application is more or less defined by the Startup class and there are no other customizations to the WebHostBuilder.

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 say you have this very contrived controller and web models:


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; }
}


public class MathController : Controller
{
    [HttpGet("/math/add/{one}/{two}")]
    public OperationResult Add(int one, int two)
    {
        return new OperationResult
        {
            Answer = one + two
        };
    }

    [HttpPut("/math")]
    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("/math")]
    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));
        }
    }
    

First off, let's test the GET method in that controller above by passing a url and verifying the results:


[Fact]
public async Task get_happy_path()
{
    // SystemUnderTest is from Alba
    // The "Startup" type would be the Startup class from your
    // web application. 
    using (var system = SystemUnderTest.ForStartup<WebApp.Startup>())
    {
        // Issue a request, and check the results
        var result = await system.GetAsJson<OperationResult>("/math/add/3/4");
        
        result.Answer.ShouldBe(7);
    }
}

So what just happened in that test? First off, the call to SystemUnderTest.For<T>() bootstraps your web application using the Startup type from your web application. Behind the scenes, Alba is using the same WebHost.CreateDefaultBuilder().UseStartup<T>() mechanism, but the difference is that Alba uses TestServer as a replacement for Kestrel (i.e., Alba does not spin up Kestrel during testing so there's no port conflicts). See Bootstrapping and Configuration for more information on advanced configuration options.

The call to system.GetAsJson<OperationResult>("/math/add/3/4") is performing these steps internally:

  1. Formulate an HttpRequest object that will be passed to the application
  2. Execute the web request against your application, which will run all configured middleware and any MVC controller classes that match the requested url
  3. Assert that the response status code is 200 OK
  4. Read the raw JSON coming off the HttpResponse
  5. Deserialize the raw JSON to the requested OperationResult type using the Json serialization settings of the running application
  6. Returns the resulting OperationResult

Alright then, let's try posting JSON in and examining the JSON out:


[Fact]
public async Task post_and_expect_response()
{
    using (var system = SystemUnderTest.ForStartup<WebApp.Startup>())
    {
        var request = new OperationRequest
        {
            Type = OperationType.Multiply,
            One = 3,
            Two = 4
        };

        var result = await system.PostJson(request, "/math")
            .Receive<OperationResult>();
        
        result.Answer.ShouldBe(12);
        result.Method.ShouldBe("POST");
    }
}

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.

Testing Hello, World

Now let's say that you built the obligatory hello world application for ASP.Net Core shown below:


public class Startup
{
    public void Configure(IApplicationBuilder builder)
    {
        builder.Run(context =>
        {
            context.Response.Headers["content-type"] = "text/plain";
            return context.Response.WriteAsync("Hello, World!");
        });
    }
}

We can now use Alba to declare an integration test for our Hello, World application within an xUnit testing project:


[Fact]
public async Task should_say_hello_world()
{
    using (var system = SystemUnderTest.ForStartup<Startup>())
    {
        // This runs an HTTP request and makes an assertion
        // about the expected content of the response
        await system.Scenario(_ =>
        {
            _.Get.Url("/");
            _.ContentShouldBe("Hello, World!");
            _.StatusCodeShouldBeOk();
        });
    }
}

The sample up above bootstraps the application defined by our Startup and executes a Scenario against the running system. A Scenario in Alba defines how the HTTP request should be constructed (the request body, headers, url) and optionally gives you the ability to express assertions against the expected HTTP response.

Alba comes with plenty of helpers in its fluent interface to work with the HttpRequest and HttpResponse, or you can work directly with the underlying ASP.Net Core objects:


[Fact]
public async Task should_say_hello_world_with_raw_objects()
{
    using (var system = SystemUnderTest.ForStartup<Startup>())
    {
        var response = await system.Scenario(_ =>
        {
            _.Get.Url("/");
            _.StatusCodeShouldBeOk();
        });

        response.ResponseBody.ReadAsText()
            .ShouldBe("Hello, World!");

        // or you can go straight at the HttpContext
        // The ReadAllText() extension method is from Baseline


        var body = response.Context.Response.Body;
        body.Position = 0; // need to rewind it because we read it above
        body.ReadAllText().ShouldBe("Hello, World!");
    }
}

Do note that Alba is not directly coupled to xUnit and would be usable within any .Net unit testing library.

To see where to go from here, see the Documentation