Testing in C#: Unit, Integration, E2E from A to Z

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 Testing is Not a Luxury, but a Necessity



In the world of modern C# (.NET) development, code quality determines not only the speed of product release but also its reputation. Errors in production, sudden crashes after a dependency update, or incorrect business logic behavior are scenarios that can be avoided with well-structured testing.



Many developers, especially beginners, perceive tests as "extra work." However, in practice, testing saves hours of debugging and refactoring. In this article, we will break down three key levels of testing in the C# ecosystem: Unit, Integration, and E2E (End-to-End). You will see how each one solves its own tasks, which tools to use, and how to write clean, maintainable tests.



1. Unit Testing: The Foundation of Quality



Unit tests check isolated blocks of code (usually a single method or class) in isolation from external dependencies. In C#, this is the fastest and most stable level of testing. The main goal is to ensure that each function works correctly with various input data.



Tools for Unit Tests in C#



The most popular frameworks for .NET are xUnit (recommended by Microsoft) and NUnit. For dependency mocking, the Moq library or the built-in .NET FakeItEasy is used.



Unit Test Example (xUnit + Moq)



Suppose we have a discount calculation service that depends on a user repository:



public class DiscountService{    private readonly IUserRepository _userRepo;

public DiscountService(IUserRepository userRepo) { _userRepo = userRepo; }

public decimal CalculateDiscount(int userId, decimal orderTotal) { var user = _userRepo.GetById(userId); if (user.IsVip) return orderTotal * 0.2m; // 20% discount return orderTotal * 0.05m; // 5% discount }}


Test for this method:



public class DiscountServiceTests{    [Fact]    public void CalculateDiscount_WhenUserIsVip_Returns20Percent()    {        // Arrange        var mockRepo = new Mock<IUserRepository>();        mockRepo.Setup(repo => repo.GetById(1))                .Returns(new User { Id = 1, IsVip = true });        var service = new DiscountService(mockRepo.Object);

// Act var result = service.CalculateDiscount(1, 100m);

// Assert Assert.Equal(20m, result); }}


Important: In unit tests, we do not access a real database. We use a mock to isolate the DiscountService logic.



2. Integration Testing: Checking Component Interaction



Integration tests check the interaction of several real components: database, API, file system. In .NET, WebApplicationFactory (for ASP.NET Core) and test containers (Testcontainers) are often used for this.



Features of Integration Tests



  • Slower than unit tests — require setting up infrastructure (DB, HTTP server).
  • More expensive to maintain — need to manage the state of the test environment.
  • Value — identify issues in configuration, data mapping, and working with a real database.


Integration Test Example (ASP.NET Core + xUnit)



Suppose we have a controller for working with orders. The test will check that a request to the real API returns correct JSON:



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

public OrdersApiTests(WebApplicationFactory<Program> factory) { _client = f

Blogs

Book Recommendations