Scenario Testing

Writing your first scenario

TIP

Alba is not directly coupled to MVC Core in any way and 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:

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:

[Fact]
public async Task get_happy_path()
{
    var builder = Program.CreateHostBuilder(Array.Empty<string>());

    await using var system = new AlbaHost(builder);
    
    // Issue a request, and check the results
    var result = await system.GetAsJson<OperationResult>("/math/add/3/4");
        
    result.Answer.ShouldBe(7);
}

snippet source | anchor

So what just happened in that test? First off, the call to new AlbaHost(IHostBuilder) bootstraps your web application.

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

  1. Formulate an 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
  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:

[Fact]
public async Task post_and_expect_response()
{
    using var system = AlbaHost.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");
}

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

As of Alba V5, the scenario support is no longer 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.

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!");
        });
    }
}

snippet source | anchor

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()
{
    await using var host = await Program
        .CreateHostBuilder(Array.Empty<string>())
        
        // This extension method is just a shorter version
        // of new AlbaHost(builder)
        .StartAlbaAsync();
    
    // 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 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 = AlbaHost.ForStartup<Startup>())
    {
        var response = await system.Scenario(_ =>
        {
            _.Get.Url("/");
            _.StatusCodeShouldBeOk();
        });

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

        // or you can go straight at the HttpContext
        Stream responseStream = response.Context.Response.Body;
        // do assertions directly on the responseStream
    }
}

snippet source | anchor