Testing in C#: From Unit to E2E — How to Build a Quality Test Architecture in .NET
Software quality directly depends on the testing system. In the C# and .NET ecosystem, there is an established testing pyramid: unit, integration, and end-to-end tests. In this article, we will break down each level with practical examples, show which libraries to use, and provide ready-made code templates.
1. Unit Testing: The Foundation of Code Stability
Unit tests verify the operation of individual methods, classes, or components in isolation from external dependencies (database, API, file system). The main goal is to quickly find regressions during refactoring.
Tools for C#
- NUnit — a classic framework with
[Test]and[TestCase]attributes. - xUnit — a modern framework from the creators of NUnit, preferred in new projects.
- Moq — a library for creating mock objects.
- FluentAssertions — for readable assertions.
Example of a unit test with xUnit and Moq
Suppose we have a discount calculation service that accesses a user repository:
public interface IUserRepository{ bool IsPremiumUser(int userId);}
public class DiscountService{ private readonly IUserRepository _userRepo; public DiscountService(IUserRepository userRepo) => _userRepo = userRepo;
public decimal CalculateDiscount(int userId, decimal amount) { if (_userRepo.IsPremiumUser(userId)) return amount * 0.9m; // 10% discount return amount; }}
// Testpublic class DiscountServiceTests{ [Fact] public void CalculateDiscount_ForPremiumUser_Returns90Percent() { // Arrange var mockRepo = new Mock<IUserRepository>(); mockRepo.Setup(r => r.IsPremiumUser(It.IsAny<int>())).Returns(true); var service = new DiscountService(mockRepo.Object);
// Act var result = service.CalculateDiscount(1, 100m);
// Assert Assert.Equal(90m, result); }}Tip: Use [Theory] and [InlineData] in xUnit for parameterized tests — this reduces code duplication.
2. Integration Testing: Verifying Component Connections
Integration tests check how your modules interact with real external systems: databases, queues, web services. Unlike unit tests, mocks are not used here — we spin up real environments.
Libraries and Approaches
- TestContainers — launches Docker containers (PostgreSQL, SQL Server, Redis) directly from tests.
- WebApplicationFactory (from Microsoft.AspNetCore.Mvc.Testing) — creates a test ASP.NET Core host.
- Respawn — cleans the database between tests without recreating the container.
Example of an integration test with TestContainers and WebApplicationFactory
Let's verify that our API correctly saves a user in PostgreSQL:
public class UserApiIntegrationTests : IClassFixture<TestContainersFixture>{ private readonly HttpClient _client; private readonly TestContainersFixture _fixture;
public UserApiIntegrationTests(TestContainersFixture fixture) { _fixture = fixture; var factory = new WebApplicationFactory<Program>() .WithWebHostBuilder(builder => { builder.ConfigureTestServices(services => { // Replace the real connection string with the container one services.RemoveAll<DbContextOptions<AppDbContext>>(); se