Skip to main content
This guide covers testing ASP.NET Core APIs running on .NET 8 or later.
If you’re using ASP.NET Web API 2 on .NET Framework, see the ASP.NET Classic testing guide instead.
Breakdance provides a powerful test base class that wraps ASP.NET Core’s TestServer, giving you in-memory HTTP testing with full dependency injection support.

Prerequisites

1

Install the package

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

Reference your API project

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

Quick Start

Inherit from AspNetCoreBreakdanceTestBase to get automatic TestServer management:
MyApiTests.cs
using CloudNimble.Breakdance.AspNetCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Net;
using System.Threading.Tasks;

[TestClass]
public class MyApiTests : AspNetCoreBreakdanceTestBase
{
    [TestInitialize]
    public void Setup()
    {
        // Configure the API services
        AddApis();
        TestSetup();
    }

    [TestMethod]
    public async Task GetUsers_ReturnsSuccess()
    {
        var client = GetHttpClient();

        var response = await client.GetAsync("/users");

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

    [TestCleanup]
    public void Cleanup()
    {
        TestTearDown();
    }
}

Configuration Methods

AspNetCoreBreakdanceTestBase provides several helper methods to configure your test environment:

AddApis()

Configures the test server with controller support (authorization, CORS, data annotations, formatter mappings):
[TestInitialize]
public void Setup()
{
    AddApis();
    TestSetup();
}
This is equivalent to calling services.AddControllers() in your Startup.cs.

AddMinimalMvc()

Registers only the minimum MVC services needed to route requests and invoke controllers:
[TestInitialize]
public void Setup()
{
    AddMinimalMvc();
    TestSetup();
}
Use this for lightweight tests where you don’t need the full controller feature set.

AddViews()

Configures support for controllers with Razor views:
[TestInitialize]
public void Setup()
{
    AddViews();
    TestSetup();
}

AddRazorPages()

Configures support for Razor Pages:
[TestInitialize]
public void Setup()
{
    AddRazorPages();
    TestSetup();
}

Custom Configuration

For more control, configure the TestHostBuilder directly:
[TestInitialize]
public void Setup()
{
    // Add custom services
    TestHostBuilder.ConfigureServices(services =>
    {
        services.AddControllers();
        services.AddScoped<IUserService, TestUserService>();
        services.AddDbContext<AppDbContext>(options =>
            options.UseInMemoryDatabase("TestDb"));
    });

    // Configure the application pipeline
    TestHostBuilder.Configure(app =>
    {
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints => endpoints.MapControllers());
    });

    // Configure app settings
    TestHostBuilder.ConfigureAppConfiguration(config =>
    {
        config.AddInMemoryCollection(new Dictionary<string, string>
        {
            ["ConnectionStrings:Default"] = "InMemory",
            ["Features:EnableNewFeature"] = "true"
        });
    });

    TestSetup();
}

Getting an HttpClient

Use GetHttpClient() to get a client connected to the in-memory test server:
[TestMethod]
public async Task GetUsers_ReturnsUserList()
{
    // Default base address is http://localhost/api/test
    var client = GetHttpClient();

    var response = await client.GetAsync("/users");
    var content = await response.Content.ReadAsStringAsync();

    Assert.IsTrue(response.IsSuccessStatusCode);
    Assert.IsFalse(string.IsNullOrWhiteSpace(content));
}

Custom Route Prefix

Override the default route prefix:
var client = GetHttpClient(routePrefix: "api/v2");

With Authentication

Add authentication headers:
using System.Net.Http.Headers;

var authHeader = new AuthenticationHeaderValue("Bearer", "your-test-token");
var client = GetHttpClient(authHeader);

var response = await client.GetAsync("/protected-resource");

Accessing Services

Resolve services from the test server’s dependency injection container:
[TestMethod]
public void CanResolveServices()
{
    var userService = GetService<IUserService>();
    Assert.IsNotNull(userService);
}

[TestMethod]
public void CanResolveMultipleImplementations()
{
    var handlers = GetServices<IMessageHandler>();
    Assert.IsTrue(handlers.Any());
}
For .NET 8+, keyed services are also supported:
[TestMethod]
public void CanResolveKeyedServices()
{
    var primaryCache = GetKeyedService<ICache>("primary");
    var secondaryCache = GetKeyedService<ICache>("secondary");

    Assert.IsNotNull(primaryCache);
    Assert.IsNotNull(secondaryCache);
}

Testing POST/PUT/PATCH Requests

[TestMethod]
public async Task CreateUser_ReturnsCreated()
{
    var client = GetHttpClient();

    var newUser = new { Name = "John Doe", Email = "[email protected]" };
    var content = new StringContent(
        JsonSerializer.Serialize(newUser),
        Encoding.UTF8,
        "application/json");

    var response = await client.PostAsync("/users", content);

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

Testing Error Responses

[TestMethod]
public async Task GetUser_NotFound_Returns404()
{
    var client = GetHttpClient();

    var response = await client.GetAsync("/users/99999");

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

[TestMethod]
public async Task CreateUser_InvalidData_Returns400()
{
    var client = GetHttpClient();

    var invalidUser = new { Name = "", Email = "not-an-email" };
    var content = new StringContent(
        JsonSerializer.Serialize(invalidUser),
        Encoding.UTF8,
        "application/json");

    var response = await client.PostAsync("/users", content);

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

Using Static Helpers

For simpler scenarios, use AspNetCoreTestHelpers without inheriting from the test base:
using CloudNimble.Breakdance.AspNetCore;
using Microsoft.AspNetCore.TestHost;

[TestMethod]
public async Task QuickTest()
{
    var testServer = await AspNetCoreTestHelpers.GetTestableHttpServerAsync(
        registration: services =>
        {
            services.AddControllers();
        },
        builder: app =>
        {
            app.UseRouting();
            app.UseEndpoints(endpoints => endpoints.MapControllers());
        });

    var client = testServer.CreateClient();
    var response = await client.GetAsync("/api/health");

    Assert.IsTrue(response.IsSuccessStatusCode);
}

Integration with FluentAssertions

Combine with FluentAssertions for more expressive tests:
using FluentAssertions;

[TestMethod]
public async Task GetUsers_ReturnsNonEmptyList()
{
    var client = GetHttpClient();

    var response = await client.GetAsync("/users");
    var content = await response.Content.ReadAsStringAsync();
    var users = JsonSerializer.Deserialize<List<User>>(content);

    response.StatusCode.Should().Be(HttpStatusCode.OK);
    users.Should().NotBeEmpty();
    users.Should().AllSatisfy(u => u.Email.Should().Contain("@"));
}

Comparison: TestServer vs Real Server

AspectTestServer (In-Memory)Real Server
SpeedFast (no network)Slower (HTTP overhead)
Port conflictsNonePossible
Full HTTP stackMost featuresYes
SSL testingLimitedYes
MiddlewareFull supportFull support
AuthenticationConfigurableFull support
Best forUnit tests, CI/CDE2E tests

Lifecycle

Understanding the test lifecycle helps avoid common issues:
[TestClass]
public class LifecycleExample : AspNetCoreBreakdanceTestBase
{
    [AssemblyInitialize]
    public static void AssemblyInit(TestContext context)
    {
        // Runs once before all tests in the assembly
    }

    [TestInitialize]
    public void TestInit()
    {
        // Configure services and middleware
        AddApis();

        // This builds and starts the TestServer
        TestSetup();
    }

    [TestMethod]
    public async Task MyTest()
    {
        // TestServer is ready to use
        var client = GetHttpClient();
        // ...
    }

    [TestCleanup]
    public void TestCleanup()
    {
        // Disposes the TestServer
        TestTearDown();
    }
}