DEV Community

Cover image for testing minimal web apis with asp.net
dorinandreidragan
dorinandreidragan

Posted on • Edited on

3

testing minimal web apis with asp.net

Testing Web APIs doesn’t need to be a chore. No sprawling frameworks. No over-engineered test setups. Just sharp, focused integration tests that give you confidence.

Let’s build them.

set up a minimal API worth testing ️

Forget databases. Forget layers of abstraction. You’re staring at a minimal book inventory API that lives entirely in memory. It’s lean. Perfect for test-driving.

Run this in your terminal:

dotnet new sln --name BooksInventory

mkdir src tests
dotnet new web -o src/BooksInventory.WebApi
dotnet new xunit -o tests/BooksInventory.WebApi.Tests

dotnet sln add src/BooksInventory.WebApi
dotnet sln add tests/BooksInventory.WebApi.Tests

dotnet add tests/BooksInventory.WebApi.Tests package FluentAssertions
dotnet add tests/BooksInventory.WebApi.Tests package Microsoft.AspNetCore.Mvc.Testing
Enter fullscreen mode Exit fullscreen mode

know what you’re testing

This API does two things. That’s it:

  • POST /addBook — accepts a title, author, and ISBN; returns a new BookId.
  • GET /books/{id} — returns the book’s details, or a 404 if it doesn’t exist.

Here’s the entire API, no fluff:

using System.Collections.Concurrent;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var books = new ConcurrentDictionary<string, Book>();

app.MapPost("/addBook", (AddBookRequest request) =>
{
    var bookId = Guid.NewGuid().ToString();
    var book = new Book(request.Title, request.Author, request.ISBN);

    if (!books.TryAdd(bookId, book))
    {
        return Results.Problem("Failed to add book due to a concurrency issue.");
    }

    return Results.Ok(new AddBookResponse(bookId));
});

app.MapGet("/books/{id}", (string id) =>
{
    if (books.TryGetValue(id, out var book))
    {
        return Results.Ok(book);
    }
    return Results.NotFound(new { Message = "Book not found", BookId = id });
});

app.Run();

public record AddBookRequest(string Title, string Author, string ISBN);
public record AddBookResponse(string BookId);
public record Book(string Title, string Author, string ISBN);

// Make Program partial for test visibility
public partial class Program { }
Enter fullscreen mode Exit fullscreen mode

write integration tests that matter 🧪

You're not mocking. You're not faking. You’re hitting the real thing using WebApplicationFactory.

using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;

namespace BooksInventory.WebApi.Tests;

public class BookInventoryTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public BookInventoryTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task AddBook_ReturnsBookId()
    {
        var request = new AddBookRequest("AI Engineering", "Chip Huyen", "1098166302");
        var content = request.GetHttpContent();

        var response = await _client.PostAsync("/addBook", content);

        response.EnsureSuccessStatusCode();
        var result = await response.DeserializeAsync<AddBookResponse>();
        result?.Should().NotBeNull();
        result!.BookId.Should().NotBeNullOrEmpty();
    }

    [Fact]
    public async Task GetBook_ReturnsBookDetails()
    {
        var addRequest = new AddBookRequest("AI Engineering", "Chip Huyen", "1234567890");
        var addResponse = await _client.PostAsync("/addBook", addRequest.GetHttpContent());
        var bookId = (await addResponse.DeserializeAsync<AddBookResponse>())?.BookId;

        var getResponse = await _client.GetAsync($"/books/{bookId}");

        getResponse.EnsureSuccessStatusCode();
        var book = await getResponse.DeserializeAsync<Book>();
        book.Should().BeEquivalentTo(
            new Book(
                addRequest.Title,
                addRequest.Author,
                addRequest.ISBN));
    }
}
Enter fullscreen mode Exit fullscreen mode

kill boilerplate with sharp extensions

Don't repeat yourself. Don’t clutter tests with serialization logic. Add these extensions and move on.

using System.Text;
using System.Text.Json;

public static class HttpContentExtensions
{
    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        PropertyNameCaseInsensitive = true
    };

    public static async Task<T?> DeserializeAsync<T>(this HttpResponseMessage response)
    {
        return JsonSerializer.Deserialize<T>(
            await response.Content.ReadAsStringAsync(),
            SerializerOptions);
    }

    public static HttpContent GetHttpContent<T>(this T obj) where T : class
    {
        return new StringContent(
            JsonSerializer.Serialize(obj),
            Encoding.UTF8, "application/json");
    }
}
Enter fullscreen mode Exit fullscreen mode

skip the tests? hit it with rest client

Not every check needs a test method. Sometimes you just want to click. The REST Client extension in VS Code makes that painless.

Create a .http file like this:

POST {{baseUrl}}/addBook HTTP/1.1
Content-Type: application/json

{
    "Title": "The Pragmatic Programmer",
    "Author": "Andy Hunt and Dave Thomas",
    "ISBN": "9780135957059"
}

###

# test GET /books/{id} (replace {id} with an actual ID)
GET {{baseUrl}}/books/{id} HTTP/1.1
Accept: application/json
Enter fullscreen mode Exit fullscreen mode

No Postman. No curl. Just fire and read. Right in your editor.

integration testing should feel like a power move ⚡

You don’t need a test framework war chest to validate your minimal API.

You need:

  • Real HTTP calls through WebApplicationFactory
  • Clean assertions from FluentAssertions
  • A few smart helpers to keep your test files tight
  • The REST Client for fast manual pokes when you feel like it

That’s it. Want to see the full source or send improvements? It’s on GitHub right here. Go break something. Then test it better.

Top comments (1)

Collapse
 
andy124 profile image
synncb

Thank you for sharing. I think it's very useful! I recently read a help document, and the method in it is also very good.support.servbay.com/dotnet/how-to-...

DEV Launches and Announcements

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

TL;DR: Eight years ago, we launched Timescale to bring time-series to PostgreSQL. Our mission was simple: help developers building time-series applications.

Check out the challenge

DEV is bringing live events to the community. Dismiss if you're not interested. ❤️