ASP.NET Web API (.NET Framework) · Quality & Delivery

Integration Testing

Unit tests check pieces; integration tests check the whole request pipeline — routing, model binding, validation, filters and JSON — exactly as a client sees it. OWIN’s TestServer runs it all in memory.

Advanced 6 min Lesson 31 of 36Covers ASP.NET Web API 2.2 on .NET Framework 4.8.
Before you start:Unit Testing
0/36 in ASP.NET Web API
On this page · 6 min · Advanced

Learning objectives#

  • Host Web API 2 in memory with Microsoft.Owin.Testing
  • Send real HTTP requests from tests
  • Check status codes and JSON bodies
  • Decide what belongs in integration vs unit tests

Explanation#

Install Microsoft.Owin.Testing and Microsoft.AspNet.WebApi.Owin in the test project. TestServer.Create<Startup>() builds the OWIN pipeline in memory — no ports, no IIS.

The test Startup creates an HttpConfiguration, calls your real WebApiConfig.Register (or the same steps) and app.UseWebApi(config).

_server.HttpClient sends real HTTP requests through routing, model binding, Data Annotations validation, filters and formatters.

ReadAsAsync<T>() and PostAsJsonAsync() (from Microsoft.AspNet.WebApi.Client) make JSON easy in tests.

Point data access at a test database (LocalDB, a Docker SQL Server) or swap repositories for in-memory ones via your DI container. Reset data between tests so they stay independent.

Integration tests are slower than unit tests; use them for the contract (URLs, status codes, JSON shape), and unit tests for detailed logic.

Syntax#

C# Illustrative
using (var server = TestServer.Create<TestStartup>())
{
    var response = await server.HttpClient.GetAsync("/api/products/1");
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Code example: In-memory tests through the full pipeline#

C#ProductsApiTests.cs Illustrative
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Owin.Testing;
using Owin;
using Xunit;
using ShopApi.Models;


// ── API code ──
public class CreateProductRequest
{
    [Required] public string Name { get; set; }
    [Range(1, 100000)] public decimal Price { get; set; }
}


public class ProductsController : ApiController
{
    private static readonly Dictionary<int, ProductDto> Store = new Dictionary<int, ProductDto>
    {
        [1] = new ProductDto { Id = 1, Name = "Pen", Price = 20m },
    };


    [HttpGet, Route("api/products/{id:int}")]
    public IHttpActionResult Get(int id)
    {
        ProductDto product;
        return Store.TryGetValue(id, out product) ? (IHttpActionResult)Ok(product) : NotFound();
    }


    [HttpPost, Route("api/products")]
    public IHttpActionResult Post(CreateProductRequest request)
    {
        if (request == null || !ModelState.IsValid) return BadRequest(ModelState);
        return Created("api/products/2", new ProductDto { Id = 2, Name = request.Name, Price = request.Price });
    }
}


// ── Test host ──
public class TestStartup
{
    public void Configuration(IAppBuilder app)
    {
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();     // in a real project: WebApiConfig.Register(config);
        app.UseWebApi(config);
    }
}


// ── Tests ──
public class ProductsApiTests : IDisposable
{
    private readonly TestServer _server = TestServer.Create<TestStartup>();


    public void Dispose() => _server.Dispose();


    [Fact]
    public async Task Get_ExistingProduct_Returns200WithJson()
    {
        var response = await _server.HttpClient.GetAsync("/api/products/1");


        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var product = await response.Content.ReadAsAsync<ProductDto>();
        Assert.Equal("Pen", product.Name);
    }


    [Fact]
    public async Task Get_NonNumericId_Returns404FromRouting()
    {
        var response = await _server.HttpClient.GetAsync("/api/products/abc");
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }


    [Fact]
    public async Task Post_MissingName_Returns400FromValidation()
    {
        var response = await _server.HttpClient.PostAsJsonAsync("/api/products", new { Price = 10 });
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }


    [Fact]
    public async Task Post_Valid_Returns201WithLocation()
    {
        var response = await _server.HttpClient.PostAsJsonAsync("/api/products", new { Name = "Bag", Price = 650 });
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        Assert.EndsWith("api/products/2", response.Headers.Location.ToString());
    }
}
Expected output
dotnet test runs the four tests against an in-memory server: Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4. The 404 comes from the {id:int} route constraint and the 400 from real Data Annotations validation — neither would be exercised by a unit test.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. TestStartup configures Web API exactly as the real app does, and UseWebApi plugs it into OWIN.
  2. TestServer.Create<TestStartup>() hosts it in memory; each test class instance gets a fresh server.
  3. GetAsync and PostAsJsonAsync send real HTTP messages through routing, binding and validation.
  4. ReadAsAsync<ProductDto>() deserialises the JSON response, so the test checks the real wire format.
  5. Dispose shuts the server down after each test.

Real-world use case#

Before upgrading Json.NET or Web API packages, a team runs 120 integration tests that pin down every URL, status code and JSON property name. Any accidental contract change fails the build.

Common mistakes#

Sharing state between tests

Tests that depend on data created by earlier tests fail randomly when run in a different order. Reset or isolate data per test.

Testing against production databases

Never. Use LocalDB, a container or in-memory repositories.

Duplicating every unit test as an integration test

Integration tests are slower. Focus them on the HTTP contract and wiring.

Best practices#

  • Reuse the real WebApiConfig in the test Startup
  • Assert on status codes, headers and JSON shape
  • Isolate test data
  • Run integration tests in CI after unit tests

Remember this#

TestServer.Create<Startup>() = the whole API in memory.

Integration tests check routing, binding, validation and JSON together.

Unit tests for logic; integration tests for the contract.

Practice exercise#

Your task: Add an integration test proving that a request with an Accept: application/json header returns Content-Type application/json.

Need a hint?
Create an HttpRequestMessage, add the Accept header, send with _server.HttpClient.SendAsync, and check response.Content.Headers.ContentType.MediaType.
Show solution
C# Illustrative
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Owin.Testing;
using Owin;
using Xunit;


public class PingController : ApiController
{
    [HttpGet, Route("api/ping")]
    public IHttpActionResult Get() => Ok(new { pong = true });
}


public class PingStartup
{
    public void Configuration(IAppBuilder app)
    {
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();
        app.UseWebApi(config);
    }
}


public class ContentTypeTests
{
    [Fact]
    public async Task Ping_WithJsonAccept_ReturnsJson()
    {
        using (var server = TestServer.Create<PingStartup>())
        {
            var request = new HttpRequestMessage(HttpMethod.Get, "/api/ping");
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));


            var response = await server.HttpClient.SendAsync(request);


            Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
        }
    }
}

Mini quiz#

Integration Testing quizMultiple choice Question 1 of 3

Which package hosts a Web API 2 pipeline in memory for tests?

Score so far: 0/0

Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.