The previous lesson assembled BiblioTech's complete API over the already-built domain, persistence, and dependency injection. Before deploying anything (Lesson 5), it's necessary to verify that this API truly works end to end, not just that each separate piece passed its own unit tests. This lesson expands Module 8's test suite with integration tests over the endpoints, goes over the most useful debugging techniques in Visual Studio and VS Code, introduces event logging with ILogger to diagnose problems once in production, and closes with a concrete quality checklist that must be passed before moving on to Lesson 5.

Contents

  1. Unit tests versus integration tests
  2. Integration tests with WebApplicationFactory
  3. Debugging in Visual Studio and VS Code: breakpoints and watch
  4. Debugging asynchronous code
  5. Basic logging with ILogger
  6. Quality checklist before deploying

  1. Unit tests versus integration tests

Module 8's unit tests (Lesson 4) verified an isolated unit of code — a Loan method, or Library.LendBookAsync with a mocked ILibraryRepository — needing nothing external. An integration test, by contrast, verifies that several real pieces working together produce the expected result: in BiblioTech's case, that a real HTTP request, going through ASP.NET Core's real routing, with the real DI container resolving the real dependencies, produces the correct response.

Unit test (Module 8) Integration test (this lesson)
What it verifies An isolated method Several real components working together
Dependencies Replaced with mocks (ILibraryRepository with Moq) Real (or a controlled test version of the database)
Speed Milliseconds Slower (starts part of the real application)
What it catches Logic errors within a unit "Wiring" errors: misregistered routes, misconfigured DI, incorrect JSON serialization

Neither replaces the other: unit tests remain the fastest and most numerous foundation (Module 8), and integration tests add a different layer of confidence, closer to how a real client will use the API.

  1. Integration tests with WebApplicationFactory

ASP.NET Core offers WebApplicationFactory<TEntryPoint>, a class designed to start the complete application in memory, with no need for a real network port or a deployment, and get an HttpClient (the same type already used in Module 5) that sends its requests directly to that in-memory application:

dotnet add BiblioTech.Tests package Microsoft.AspNetCore.Mvc.Testing
using Microsoft.AspNetCore.Mvc.Testing;

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

    public ApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient(); // HttpClient pointing at the in-memory app
    }

    [Fact]
    public async Task GetBooks_ReturnsStatusCode200()
    {
        // Act
        HttpResponseMessage response = await _client.GetAsync("/books");

        // Assert
        response.EnsureSuccessStatusCode(); // throws if the code isn't 2xx
    }

    [Fact]
    public async Task PostLoans_WithNonexistentBook_Returns400()
    {
        // Arrange
        LoanRequest request = new LoanRequest("000-00-000-0000-0", 1);

        // Act
        HttpResponseMessage response = await _client.PostAsJsonAsync("/loans", request);

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }
}

IClassFixture<WebApplicationFactory<Program>> tells xUnit to share a single instance of the in-memory application across all tests in this class, instead of starting it up again for every method (much faster). PostAsJsonAsync serializes request to JSON automatically with System.Text.Json (Module 5), exactly as a real client against the deployed API would. These tests complement, without replacing, the Moq unit tests from 08-04: they verify that the HTTP endpoint truly routes, deserializes, and responds as expected — something a mocked ILibraryRepository can't check on its own.

  1. Debugging in Visual Studio and VS Code: breakpoints and watch

When a test fails or the observed behavior doesn't match what's expected, debugging lets you stop execution at a specific point and inspect the program's state at that instant, instead of guessing what's happening from console messages:

Tool What it does How it's used
Breakpoint Stops execution right before running a specific line Click in the editor's left margin, next to the line number
Watch window Shows the current value of a variable or expression while the program is stopped Type the expression (loan.ReturnDate, catalog.Count) into the Watch panel
Step Over / Step Into / Step Out Advances execution line by line, entering or not entering calls to other methods Function keys (F10/F11/Shift+F11 in Visual Studio; equivalents in VS Code)
Call Stack Shows the full chain of methods that led to the current point Dedicated panel, automatically visible when stopped at a breakpoint

A typical debugging flow over POST /loans/{id}/return (09-03, section 6): set a breakpoint on the loan.RegisterReturn(); line, launch the API in debug mode (F5 in Visual Studio or VS Code), make the real request with Swagger or curl, and once execution stops, inspect loan.LoanDate and policy.CalculatePenalty(...) in the Watch window before they run, to confirm the incoming data is as expected.

  1. Debugging asynchronous code

Debugging async/await methods (Module 4) has one quirk: when you Step Into an await, execution can "jump" in a way that makes it look like the thread has changed, because it can indeed resume on a different thread after the wait (Module 6, multithreading). A few practices help keep this from being confusing:

  • Place the breakpoint after the await (for example, right where the result of GetMetadataByIsbnAsync is used) to inspect the already-resolved value, instead of trying to step through the wait itself.
  • Check the call stack carefully after an await: it may appear shorter than expected, because part of the "previous" stack, from before the wait, no longer exists in the same form once the method resumes.
  • For LendBookAsync's Task.Delay(1000) (08-04), a breakpoint right after that line confirms that execution actually waited, with no need to step through the wait itself.

  1. Basic logging with ILogger

Breakpoints are the right tool while developing and the problem can be reproduced locally. In production (Lesson 5), there's no way to stop execution with a breakpoint: the equivalent tool is logging, messages the program writes continuously describing what it's doing, so they can be reviewed later if something fails. ASP.NET Core automatically injects an ILogger<T> into any class that declares it by constructor (08-03, constructor injection):

app.MapPost("/loans/{id}/return", (
    int id, Library library, IPenaltyPolicy policy, ILogger<Program> logger) =>
{
    Loan? loan = library.Loans.ElementAtOrDefault(id);

    if (loan is null)
    {
        logger.LogWarning("Attempted return of a nonexistent loan: {Id}", id);
        return Results.NotFound($"Loan #{id} doesn't exist.");
    }

    loan.RegisterReturn();
    decimal penalty = loan.CalculatePenalty(policy);

    logger.LogInformation(
        "Return registered for loan {Id}. Penalty applied: {Penalty:C}", id, penalty);

    library.SaveCatalog();
    return Results.Ok(new { loan.ReturnDate, Penalty = penalty });
});
Level When to use it
LogInformation Normal events worth tracking later (a loan was registered, a return was processed)
LogWarning Something unexpected but not serious: a request with data that wasn't found, a retry
LogError A real failure that prevented completing the operation (a caught exception, a downed external service)

The {Id} and {Penalty:C} placeholders in the message are not C# string interpolation ($"..."): they're named parameters that the logging provider structures separately from the text, which lets you later search or filter logs by Id without having to parse the message text. By default, ASP.NET Core writes these messages to the console during development; in production (Lesson 5), additional providers (files, monitoring services) are configured without changing a single line of the logger.LogInformation(...) calls.

  1. Quality checklist before deploying

Before moving on to Lesson 5 (Deployment), this checklist brings together what's been verified in this lesson with what was already covered in 08-05:

  • [ ] All of Module 8's unit tests (Loan, Library.LendBookAsync) are passing.
  • [ ] The new integration tests from section 2 over the main endpoints are passing.
  • [ ] The project's code analyzer reports no unjustified warnings (dotnet build with no warnings, or each one explicitly reviewed).
  • [ ] 08-05's review checklist (naming, injected dependencies, consistent nullability) has been applied to 09-03's new code.
  • [ ] Endpoints that can fail for expected reasons (unavailable book, member not found, external metadata unavailable) return a clear HTTP code and message, not a generic 500 error.
  • [ ] Logging (ILogger) has been added at least at the points where something can fail for external reasons (persistence, metadata lookup).

Passing this checklist isn't a bureaucratic formality: it's the concrete confirmation that the API is ready for the next step, much riskier if any of this were missing: publishing it outside the development environment.

Common Mistakes and Tips

  • Confusing an integration test with a manual test: an integration test must be runnable automatically with dotnet test, just like a unit test; testing the API by hand with Swagger is useful during development, but it doesn't replace an automated test that repeats on every change.
  • Using string interpolation ($"...") in ILogger calls: this loses the structure of the named parameters (section 5), making it harder to search or filter logs afterward; always use the {ParameterName} placeholders with the values as additional arguments.
  • Debugging in production with breakpoints: this isn't a real option (there's no way to "stop" a production server without interrupting users); the logging from section 5 is the tool designed exactly for that scenario.
  • Tip: if an integration test fails intermittently (sometimes passes, sometimes doesn't), suspect real-time dependencies first (like LendBookAsync's Task.Delay) or shared state between tests that isn't cleaned up properly from one to the next.

Exercises

  1. Write an integration test PostMembers_WithValidMember_Returns201 that does a POST to /members with a valid Member and checks that the response has status code 201 Created.

  2. Add a call to logger.LogError in the catch (InvalidOperationException ex) block of the POST /loans endpoint from 09-03, logging the exception's message before returning Results.Conflict(ex.Message).

Solutions

[Fact]
public async Task PostMembers_WithValidMember_Returns201()
{
    // Arrange
    Member member = new Member(99, "Marta Ruiz");

    // Act
    HttpResponseMessage response = await _client.PostAsJsonAsync("/members", member);

    // Assert
    Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
try
{
    await library.LendBookAsync(book, member);
}
catch (InvalidOperationException ex)
{
    logger.LogError(ex, "Failed to register the loan for ISBN {Isbn}", request.Isbn);
    return Results.Conflict(ex.Message);
}

(Note: this version requires adding ILogger<Program> logger as an additional endpoint parameter, just like in section 5's example; logger.LogError(ex, ...) also logs the full exception, not just the message.)

Conclusion

This lesson has expanded confidence in BiblioTech beyond Module 8's unit tests: there are now also integration tests with WebApplicationFactory that verify the real endpoints end to end, debugging techniques have been reviewed — including the quirk of debugging asynchronous code — logging with ILogger has been added to diagnose problems once the application is deployed, and a concrete quality checklist has been set. With all those points passed, BiblioTech is finally ready for the step that closes out the project and the entire course: the final lesson, Deployment, will publish this API, containerize it with Docker, and configure its behavior per environment for production.

© Copyright 2026. All rights reserved