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

AspectUnit TestIntegration Test
ScopeOne method/classFull request to response
DatabaseMockedReal (in-memory or test DB)
HTTPNot involvedFull HTTP stack tested
SpeedMillisecondsSeconds
What it catchesLogic bugsWiring 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.

Leave a Comment