Skip to main content
This guide covers testing ASP.NET Web API 2 controllers that run on the .NET Framework (4.6.2+).
If you’re using ASP.NET Core (running on .NET 6+), see the ASP.NET Core testing guide instead.
Testing Web API controllers traditionally requires running a full HTTP server, which is slow and can have port conflicts. Breakdance provides helpers that create an in-memory HTTP pipeline, letting you test your controllers directly without network overhead.

Prerequisites

1

Install the package

Add the Breakdance WebApi package to your test project:
dotnet add package Breakdance.WebApi
2

Reference your Web API project

Your test project needs a reference to the project containing your controllers:
<ProjectReference Include="..\MyApi\MyApi.csproj" />

Quick Start

The simplest approach uses WebApiTestHelpers to get a fully configured HttpClient:
MyApiTests.cs
using CloudNimble.Breakdance.WebApi;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

[TestClass]
public class MyApiTests
{
    [TestMethod]
    public async Task GetUsers_ReturnsSuccessStatusCode()
    {
        // Get an HttpClient wired up to your API's in-memory pipeline
        var httpClient = WebApiTestHelpers.GetTestableHttpClient();

        // Make requests just like you would against a real server
        var response = await httpClient.ExecuteTestRequest(
            HttpMethod.Get,
            resource: "/api/users");

        // Assert
        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    }
}
The ExecuteTestRequest extension method handles building the request URL and headers for you. It defaults to http://localhost/api/test as the base path.

How It Works

When you call GetTestableHttpClient(), Breakdance creates:
  1. An HttpConfiguration with attribute routing enabled
  2. An HttpServer that processes requests in-memory
  3. An HttpClient connected to that server
// These three lines are equivalent to calling WebApiTestHelpers.GetTestableHttpClient()
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
var server = new HttpServer(config);
var client = new HttpClient(server);
Your controllers are discovered automatically from referenced assemblies.

Configuration Options

Custom HttpConfiguration

If your API requires specific configuration (like custom routes or services), use the extension methods on HttpConfiguration:
[TestMethod]
public async Task CustomConfiguration_Works()
{
    var config = new HttpConfiguration();

    // Add your custom routes
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    // Register your DI container
    config.DependencyResolver = new MyDependencyResolver();

    // Get a client using your custom config
    var client = config.GetTestableHttpClient();

    var response = await client.ExecuteTestRequest(
        HttpMethod.Get,
        routePrefix: "api",  // Match your route template
        resource: "/products");

    Assert.IsTrue(response.IsSuccessStatusCode);
}

Changing the Route Prefix

The default route prefix is api/test. Override it to match your API’s routes:
var response = await client.ExecuteTestRequest(
    HttpMethod.Get,
    routePrefix: "api/v1",
    resource: "/customers");

Custom Host

If localhost conflicts with something on your machine:
var response = await client.ExecuteTestRequest(
    HttpMethod.Get,
    host: "http://testhost",
    resource: "/api/orders");

Sending Request Bodies

For POST, PUT, and PATCH requests, pass a payload object:
[TestMethod]
public async Task CreateUser_WithValidPayload_Returns201()
{
    var client = WebApiTestHelpers.GetTestableHttpClient();

    var newUser = new { Name = "John Doe", Email = "[email protected]" };

    var response = await client.ExecuteTestRequest(
        HttpMethod.Post,
        resource: "/api/users",
        payload: newUser);

    Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
}
The payload is automatically serialized to JSON using Newtonsoft.Json.

Reading Response Content

[TestMethod]
public async Task GetUser_ReturnsUserData()
{
    var client = WebApiTestHelpers.GetTestableHttpClient();

    var response = await client.ExecuteTestRequest(
        HttpMethod.Get,
        resource: "/api/users/1");

    // Read the response body
    var content = await response.Content.ReadAsStringAsync();
    Assert.IsFalse(string.IsNullOrWhiteSpace(content));

    // Or deserialize directly
    var user = JsonConvert.DeserializeObject<User>(content);
    Assert.AreEqual(1, user.Id);
}

Testing Error Responses

In-memory testing surfaces the same error responses your API would return in production:
[TestMethod]
public async Task GetUser_NotFound_Returns404()
{
    var client = WebApiTestHelpers.GetTestableHttpClient();

    var response = await client.ExecuteTestRequest(
        HttpMethod.Get,
        resource: "/api/users/99999");

    Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
}

[TestMethod]
public async Task CreateUser_InvalidData_Returns400()
{
    var client = WebApiTestHelpers.GetTestableHttpClient();

    var invalidUser = new { Name = "", Email = "not-an-email" };

    var response = await client.ExecuteTestRequest(
        HttpMethod.Post,
        resource: "/api/users",
        payload: invalidUser);

    Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
}
Because IncludeErrorDetailPolicy.Always is set, you’ll get detailed error messages in the response body. This is helpful for debugging test failures but should not be enabled in production.

Comparison: In-Memory vs Real Server Testing

AspectIn-Memory TestingReal Server Testing
SpeedFast (no network)Slower (HTTP overhead)
Port conflictsNonePossible
Full HTTP stackNo (no network layer)Yes
SSL testingNoYes
Message handlersFull supportFull support
Best forUnit tests, CI/CDIntegration tests, E2E

Limitations

The Breakdance.WebApi package is specifically for ASP.NET Web API 2 on .NET Framework. It does not support: