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.
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.
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.
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);}
[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);}
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.