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
- Unit tests versus integration tests
- Integration tests with
WebApplicationFactory - Debugging in Visual Studio and VS Code: breakpoints and watch
- Debugging asynchronous code
- Basic logging with
ILogger - Quality checklist before deploying
- 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.
- Integration tests with
WebApplicationFactory
WebApplicationFactoryASP.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:
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.
- 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.
- 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 ofGetMetadataByIsbnAsyncis 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'sTask.Delay(1000)(08-04), a breakpoint right after that line confirms that execution actually waited, with no need to step through the wait itself.
- Basic logging with
ILogger
ILoggerBreakpoints 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.
- 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 buildwith 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 (
$"...") inILoggercalls: 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'sTask.Delay) or shared state between tests that isn't cleaned up properly from one to the next.
Exercises
-
Write an integration test
PostMembers_WithValidMember_Returns201that does aPOSTto/memberswith a validMemberand checks that the response has status code201 Created. -
Add a call to
logger.LogErrorin thecatch (InvalidOperationException ex)block of thePOST /loansendpoint from 09-03, logging the exception's message before returningResults.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.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
