Testing in C#: Unit, Integration, E2E — Advanced Level

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Write Tests at an Advanced Level?

The beginner level of testing in C# is usually limited to simple Assert.Equal and isolated unit tests. However, in real enterprise projects, this is not enough. Bugs often occur at the intersection of components — when interacting with a database, external APIs, or in complex business logic with side effects.

Advanced testing in C# (.NET) is the ability to build a test pyramid, where unit tests are fast and isolated, integration tests verify real interactions, and E2E tests simulate user behavior. In this article, we will look at modern tools: xUnit, Moq, FluentAssertions, Testcontainers, and WebApplicationFactory. You will learn how to write reliable tests that don't break during refactoring and give you confidence in your code.



1. Advanced Unit Tests: Mocks and Stub Objects

Unit tests should be isolated from external dependencies. At an advanced level, we use libraries for creating mock objects, such as Moq or NSubstitute.



Proper Mock Object Setup

Instead of creating stubs manually, use Mock.Of<T>() for quick setup. Example with Moq and FluentAssertions:

public interface IEmailService{    Task SendAsync(string to, string subject);}

public class NotificationService{ private readonly IEmailService _emailService;

public NotificationService(IEmailService emailService) { _emailService = emailService; }

public async Task NotifyUserAsync(string email) { await _emailService.SendAsync(email, "Welcome!"); }}

// Test[Fact]public async Task NotifyUserAsync_ShouldSendEmail(){ // Arrange var mockEmailService = new Mock<IEmailService>(); var service = new NotificationService(mockEmailService.Object);

// Act await service.NotifyUserAsync("test@example.com");

// Assert mockEmailService.Verify( x => x.SendAsync("test@example.com", "Welcome!"), Times.Once);}


Avoid Testing Implementation Details

The advanced approach is to test behavior, not internal calls. Use Verify only for critically important dependencies. For the rest, check the result.



2. Integration Testing: Database and External Services

Integration tests verify that multiple modules work together. For .NET Core/5+, the ideal tools are WebApplicationFactory (for ASP.NET Core) and Testcontainers (for containerizing dependencies).



Setting up WebApplicationFactory with a Real Database

Instead of using an InMemory provider (which behaves differently than a real database), use Testcontainers to run PostgreSQL or SQL Server in Docker:

public class IntegrationTestFixture : IAsyncLifetime{    private readonly TestcontainersContainer _dbContainer;

public string ConnectionString { get; private set; }

public IntegrationTestFixture() { _dbContainer = new TestcontainersBuilder<TestcontainersContainer>() .WithImage("postgres:15") .WithPortBinding(5432, true) .WithEnvironment("POSTGRES_USER", "test") .WithEnvironment("POSTGRES_PASSWORD", "test") .WithEnvironment("POSTGRES_DB", "testdb") .Build(); }

public async Task InitializeAsync() { await _dbContainer.StartAsync(); ConnectionString = $"Host=localhost;Port={_dbContainer.GetMappedPublicPort(5432)};" + $"Database=testdb;Username=test;Password=test"; }

public async Task DisposeAsync() { await _dbConta

Blogs

Book Recommendations