TDD в C#: перестань бояться тестов и начни писать код, который не стыдно показать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему "сначала тесты" — это не мазохизм, а суперсила

Представьте: вы пишете код, всё работает. Вы счастливы. Через месяц возвращаетесь к проекту, меняете одну строчку — и всё ломается. Знакомо? Добро пожаловать в клуб разработчиков, которые не пишут тесты. А теперь представьте другую реальность: вы меняете код, запускаете тесты — и через 10 секунд знаете, что ничего не сломалось. Это не магия, это TDD (Test-Driven Development) — разработка через тестирование.

В этой статье мы разберём TDD на примере C#. Без скучной теории. Только практика, код и реальные кейсы. Если вы думаете, что TDD — это для "чистых архитекторов" или "энтерпрайз-монстров", — вы удивитесь. Это инструмент, который спасёт ваш код и нервы.

Что такое TDD и как работает Red-Green-Refactor

TDD — это не про тестирование. Это про проектирование. Вы не пишете тесты после кода, а наоборот: сначала тест, потом минимальный код, который его проходит, и только потом рефакторинг. Этот цикл называется Red-Green-Refactor:

  • Red — пишем тест, который заведомо не пройдёт (красный).
  • Green — пишем минимальный код, чтобы тест прошёл (зелёный).
  • Refactor — улучшаем код, не меняя его поведение.

Кажется странным? Давайте на примере. Допустим, нам нужен калькулятор с методом сложения. Обычно мы пишем класс, потом тест. В TDD — наоборот.

Шаг 1: Red — пишем тест, который упадёт

using NUnit.Framework;

[TestFixture]
public class CalculatorTests
{
    [Test]
    public void Add_ShouldReturnSumOfTwoNumbers()
    {
        // Arrange
        var calculator = new Calculator();
        
        // Act
        var result = calculator.Add(2, 3);
        
        // Assert
        Assert.AreEqual(5, result);
    }
}

Запускаем — тест падает, потому что класса Calculator не существует. Красный — это нормально. Теперь наша задача — сделать зелёный.

Шаг 2: Green — минимум кода, чтобы тест прошёл

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b; // Минимальная реализация
    }
}

Тест зелёный. Ура. Но код пока уродливый. Переходим к шагу 3.

Шаг 3: Refactor — улучшаем, но не ломаем

На этом этапе мы можем добавить проверки, обработку исключений или изменить архитектуру. Главное — тесты должны оставаться зелёными.

public class Calculator
{
    public int Add(int a, int b)
    {
        if (a < 0 || b < 0)
            throw new ArgumentException("Only positive numbers allowed");
        return a + b;
    }
}

Тест упадёт? Да. Значит, нужно обновить тест или изменить логику. В TDD рефакторинг — это всегда диалог с тестами.

Как выбрать фреймворк для TDD в C#: NUnit vs xUnit

В C# есть три популярных фреймворка: NUnit, xUnit и MSTest. Мы рассмотрим первые два, так как они самые гибкие.

Критерий NUnit xUnit
Атрибуты [Test], [SetUp] [Fact], [Theory]
Параметризация [TestCase] [InlineData]
Изоляция тестов Новый экземпляр класса на каждый тест Новый экземпляр класса на каждый тест

Лично я рекомендую xUnit для новых проектов — он чище и лучше интегрируется с .NET Core. Но если команда привыкла к NUnit — не переживайте, оба фреймворка отлично поддерживают TDD.

Реальный пример: пишем систему заказов через TDD

Представьте, что мы разрабатываем интернет-магазин. Нужен класс OrderService, который рассчитывает скидку. Правило: если сумма заказа больше 1000 рублей — скидка 10%. Давайте пройдём TDD-цикл.

Итерация 1: базовая скидка

Пишем тест:

[Fact]
public void CalculateDiscount_WhenOrderTotalAbove1000_ShouldReturn10Percent()
{
    var service = new OrderService();
    var result = service.CalculateDiscount(1500);
    Assert.Equal(150, result);
}

Тест красный. Пишем минимальный код:

public class OrderService
{
    public decimal CalculateDiscount(decimal total)
    {
        if (total > 1000)
            return total * 0.1m;
        return 0;
    }
}

Зелёный. Пока всё просто.

Итерация 2: граничные условия

TDD требует тестировать границы. Что, если сумма ровно 1000? По логике — скидки нет. Пишем тест:

[Fact]
public void CalculateDiscount_WhenOrderTotalIs1000_ShouldReturnZero()
{
    var service = new OrderService();
    var result = service.CalculateDiscount(1000);
    Assert.Equal(0, result);
}

Код уже проходит — зелёный. Но мы не останавливаемся.

Итерация 3: отрицательная сумма

Что, если кто-то передаст отрицательную сумму? TDD заставляет думать о краевых случаях заранее.

[Fact]
public void CalculateDiscount_WhenNegativeTotal_ShouldThrowException()
{
    var service = new OrderService();
    Assert.Throws<ArgumentException>(() => service.CalculateDiscount(-100));
}

Красный. Добавляем проверку:

public decimal CalculateDiscount(decimal total)
{
    if (total < 0)
        throw new ArgumentException("Total cannot be negative");
    if (                

Блоги

Рекомендаций по книгам