This guide covers testing ASP.NET Web API 2 controllers that run on the .NET Framework (4.6.2+).
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
Install the package
Add the Breakdance WebApi package to your test project:dotnet add package Breakdance.WebApi
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:
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:
- An
HttpConfiguration with attribute routing enabled
- An
HttpServer that processes requests in-memory
- 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
| Aspect | In-Memory Testing | Real Server Testing |
|---|
| Speed | Fast (no network) | Slower (HTTP overhead) |
| Port conflicts | None | Possible |
| Full HTTP stack | No (no network layer) | Yes |
| SSL testing | No | Yes |
| Message handlers | Full support | Full support |
| Best for | Unit tests, CI/CD | Integration tests, E2E |
Limitations
The Breakdance.WebApi package is specifically for ASP.NET Web API 2 on .NET Framework. It does not support: