In this lesson, you will learn how to define the structural boundaries between isolated unit tests and system-wide integration tests to ensure your test suite remains maintainable and performant.
The Boundary of Isolation
Up until now, we have focused on Unit Testing—tests that exercise a single class or method in complete isolation. We achieved this by using Moq or NSubstitute to fake dependencies like IRepository or IHttpClientFactory. These tests are fast because they stay within the memory of your process.
An Integration Test, by contrast, crosses these boundaries. The transition from "Unit" to "Integration" occurs the moment you allow your test to interact with a real, shared infrastructure component rather than a mock. This typically includes:
- Real database connections (e.g., SQL Server or SQLite).
- File system interactions.
- Network calls to real external APIs.
- The full
WebApplicationFactorystack for your ASP.NET Core controllers.
Visualizing the Pyramid
Think of the testing pyramid as a filter for complexity. At the base, you have hundreds of unit tests verifying logic. In the middle, you have a smaller set of integration tests verifying that your DbContext configuration matches your database schema or that your HttpClient properly serializes responses.
Consider this contrast in your test project:
// Unit Test: Uses a Mock - Fast, isolated, internal logic only
[Fact]
public void CalculateDiscount_WithMockRepo_ReturnsCorrectValue()
{
var mockRepo = new Mock<IProductRepository>();
var service = new PricingService(mockRepo.Object);
// ... test logic
}
// Integration Test: Uses real DB - Slower, verifies wiring
[Fact]
public async Task GetProduct_WithRealDatabase_ReturnsActualRecord()
{
using var factory = new WebApplicationFactory<Program>();
var client = factory.CreateClient();
var response = await client.GetAsync("/products/1");
// ... asserts against live DB state
}
If you start "integration testing" by mocking your database access layer, you haven't actually integrated with the database; you’ve just written a slow unit test. True integration tests must touch the "real" underlying resources to confirm that the wiring between your code and your environment is functional.
Key Takeaway: Unit tests verify internal logic through isolation, while integration tests verify the reliability of the boundaries between your code and the external infrastructure.