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.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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());
}
}
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#
- TestStartup configures Web API exactly as the real app does, and UseWebApi plugs it into OWIN.
- TestServer.Create<TestStartup>() hosts it in memory; each test class instance gets a fresh server.
- GetAsync and PostAsJsonAsync send real HTTP messages through routing, binding and validation.
- ReadAsAsync<ProductDto>() deserialises the JSON response, so the test checks the real wire format.
- 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#
Tests that depend on data created by earlier tests fail randomly when run in a different order. Reset or isolate data per test.
Never. Use LocalDB, a container or in-memory repositories.
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?
Show solution
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#
Which package hosts a Web API 2 pipeline in memory for tests?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.