Core API Integration Testing
Unit tests verify individual methods in isolation. Integration tests test the full request-response cycle — from the HTTP request all the way through the middleware, controller, service, repository, and back. Integration tests for the BookStore API confirm that these layers work correctly together as a complete system.
Unit Testing vs Integration Testing
| Aspect | Unit Test | Integration Test |
|---|---|---|
| Scope | One method/class | Full request to response |
| Database | Mocked | Real (in-memory or test DB) |
| HTTP | Not involved | Full HTTP stack tested |
| Speed | Milliseconds | Seconds |
| What it catches | Logic bugs | Wiring bugs, routing, auth |
Step 1 – Create the Integration Test Project
dotnet new xunit -n BookStoreAPI.IntegrationTests
cd BookStoreAPI.IntegrationTests
dotnet add reference ../BookStoreAPI/BookStoreAPI.csproj
dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package FluentAssertions
Step 2 – Create a Custom Web Application Factory
WebApplicationFactory<T> starts the real ASP.NET Core application in-process, but with an in-memory database instead of SQL Server — fast, isolated, and no setup needed.
// BookStoreAPI.IntegrationTests/BookStoreWebFactory.cs
using BookStoreAPI.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace BookStoreAPI.IntegrationTests
{
public class BookStoreWebFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Remove the real SQL Server DbContext registration
var dbContextDescriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<BookStoreDbContext>));
if (dbContextDescriptor != null)
services.Remove(dbContextDescriptor);
// Replace with in-memory database
services.AddDbContext<BookStoreDbContext>(options =>
{
options.UseInMemoryDatabase("BookStoreTestDB");
});
// Seed test data
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<BookStoreDbContext>();
db.Database.EnsureCreated();
SeedTestData(db);
});
}
private static void SeedTestData(BookStoreDbContext db)
{
db.Books.AddRange(
new Models.Book { Id = 1, Title = "Clean Code", Author = "Robert C. Martin",
Price = 29.99m, Category = "Technology",
IsAvailable = true, CreatedDate = new DateTime(2024, 1, 1) },
new Models.Book { Id = 2, Title = "The Pragmatic Programmer", Author = "David Thomas",
Price = 34.99m, Category = "Technology",
IsAvailable = true, CreatedDate = new DateTime(2024, 1, 2) }
);
db.SaveChanges();
}
}
}
Step 3 – Write Integration Tests for the Books Endpoints
// BookStoreAPI.IntegrationTests/Controllers/BooksControllerTests.cs
using System.Net;
using System.Net.Http.Json;
using BookStoreAPI.DTOs;
using FluentAssertions;
namespace BookStoreAPI.IntegrationTests.Controllers
{
public class BooksControllerTests : IClassFixture<BookStoreWebFactory>
{
private readonly HttpClient _client;
public BooksControllerTests(BookStoreWebFactory factory)
{
_client = factory.CreateClient();
}
// TEST 1: GET /api/v2/books returns 200 and a list of books
[Fact]
public async Task GetAll_ShouldReturn200AndBooks()
{
// Act
var response = await _client.GetAsync("/api/v2/books");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<dynamic>();
body.Should().NotBeNull();
}
// TEST 2: GET /api/v2/books/1 returns the correct book
[Fact]
public async Task GetById_WhenExists_ShouldReturn200AndBook()
{
// Act
var response = await _client.GetAsync("/api/v2/books/1");
var book = await response.Content.ReadFromJsonAsync<BookResponseDto>();
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
book.Should().NotBeNull();
book!.Id.Should().Be(1);
book.Title.Should().Be("Clean Code");
}
// TEST 3: GET /api/v2/books/999 returns 404
[Fact]
public async Task GetById_WhenNotFound_ShouldReturn404()
{
// Act
var response = await _client.GetAsync("/api/v2/books/999");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
// TEST 4: POST /api/v2/books without token returns 401
[Fact]
public async Task Create_WithoutToken_ShouldReturn401()
{
// Arrange
var newBook = new BookCreateDto
{
Title = "Design Patterns",
Author = "Gang of Four",
Price = 45.00m,
Category = "Technology",
IsAvailable = true
};
// Act
var response = await _client.PostAsJsonAsync("/api/v2/books", newBook);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
// TEST 5: POST /api/v2/books with invalid data returns 400
[Fact]
public async Task Create_WithInvalidData_ShouldReturn400()
{
// Arrange: empty title should fail validation
var invalidBook = new BookCreateDto
{
Title = "", // invalid — required
Author = "Test",
Price = -5, // invalid — must be > 0
Category = "Technology",
IsAvailable = true
};
// Act (anonymous request — validation fires before auth in this case)
var response = await _client.PostAsJsonAsync("/api/v2/books", invalidBook);
// Assert — either 400 (validation) or 401 (unauth), both are acceptable
response.StatusCode.Should().BeOneOf(HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized);
}
}
}
Step 4 – Integration Test with Authentication
To test protected endpoints, a real JWT token must be obtained first and added to the request:
// BookStoreAPI.IntegrationTests/Controllers/BooksControllerAuthTests.cs
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using BookStoreAPI.DTOs;
using FluentAssertions;
namespace BookStoreAPI.IntegrationTests.Controllers
{
public class BooksControllerAuthTests : IClassFixture<BookStoreWebFactory>
{
private readonly HttpClient _client;
public BooksControllerAuthTests(BookStoreWebFactory factory)
{
_client = factory.CreateClient();
}
private async Task<string> GetAdminTokenAsync()
{
var loginDto = new LoginDto
{
Email = "admin@bookstore.com",
Password = "Admin123"
};
var loginResponse = await _client.PostAsJsonAsync("/api/auth/login", loginDto);
loginResponse.EnsureSuccessStatusCode();
var result = await loginResponse.Content.ReadFromJsonAsync<Dictionary<string, string>>();
return result!["token"];
}
// TEST: POST with valid admin token returns 201
[Fact]
public async Task Create_WithAdminToken_ShouldReturn201()
{
// Arrange
var token = await GetAdminTokenAsync();
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var newBook = new BookCreateDto
{
Title = "Domain-Driven Design",
Author = "Eric Evans",
Price = 49.99m,
Category = "Technology",
IsAvailable = true
};
// Act
var response = await _client.PostAsJsonAsync("/api/v2/books", newBook);
var created = await response.Content.ReadFromJsonAsync<BookResponseDto>();
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
created.Should().NotBeNull();
created!.Title.Should().Be("Domain-Driven Design");
created.Id.Should().BeGreaterThan(0);
}
// TEST: DELETE with admin token returns 204
[Fact]
public async Task Delete_WithAdminToken_ShouldReturn204()
{
// Arrange
var token = await GetAdminTokenAsync();
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await _client.DeleteAsync("/api/v2/books/1");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
}
}
}
Running Integration Tests
dotnet test BookStoreAPI.IntegrationTests
// Output:
Passed! - 7 tests in 1.45s
Integration Test Coverage for BookStore API
Endpoint Test Scenario Expected
─────────────────────────────────────────────────────────────────────────
GET /api/v2/books No auth needed 200 + list
GET /api/v2/books/1 Book exists 200 + book
GET /api/v2/books/999 Book missing 404
POST /api/v2/books No token 401
POST /api/v2/books Invalid data 400
POST /api/v2/books Admin token + valid data 201
PUT /api/v2/books/1 User token (not Admin) 403
DELETE /api/v2/books/1 Admin token 204
POST /api/auth/login Wrong password 401
POST /api/auth/login Correct credentials 200 + token
Key Points
- Integration tests test the full request-response pipeline — middleware, routing, controller, service, and repository all working together.
WebApplicationFactory<Program>spins up the real application in-process, replacing SQL Server with an in-memory database.- Integration tests are slower than unit tests but catch bugs that unit tests cannot — routing misconfigurations, missing middleware, and auth problems.
- Authenticated endpoint tests require obtaining a real JWT token by calling the login endpoint first.
- Run both unit and integration tests together with
dotnet test— they complement each other.
