← Назад к вопросам

Как писать код чтобы его легко тестировать?

2.2 Middle🔥 162 комментариев
#Тестирование

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Принципы написания легко тестируемого кода

Написание легко тестируемого кода — это не отдельная техника, а следствие соблюдения SOLID-принципов и применения архитектурных паттернов. Вот ключевые подходы:

1. Внедрение зависимостей (Dependency Injection)

Зависимости должны передаваться извне, а не создаваться внутри класса. Это позволяет заменять реальные реализации моками или стабами в тестах.

// ПЛОХО: зависимость создается внутри класса
public class OrderProcessor
{
    private readonly EmailService _emailService = new EmailService();
    
    public void Process(Order order)
    {
        // ... логика обработки
        _emailService.SendConfirmation(order);
    }
}

// ХОРОШО: зависимость внедряется через конструктор
public class OrderProcessor
{
    private readonly IEmailService _emailService;
    
    public OrderProcessor(IEmailService emailService)
    {
        _emailService = emailService;
    }
    
    public void Process(Order order)
    {
        // ... логика обработки
        _emailService.SendConfirmation(order);
    }
}

2. Принцип единственной ответственности (Single Responsibility)

Каждый класс должен решать только одну задачу. Это упрощает написание юнит-тестов, так как тестируется одна конкретная функциональность.

// ПЛОХО: класс делает слишком много
public class ReportGenerator
{
    public void GenerateReport(Data data)
    {
        // Валидация данных
        // Преобразование формата
        // Сохранение в базу
        // Отправка по email
    }
}

// ХОРОШО: разделение ответственностей
public class DataValidator { /* ... */ }
public class DataTransformer { /* ... */ }
public class ReportSaver { /* ... */ }
public class EmailNotifier { /* ... */ }

3. Изоляция бизнес-логики от инфраструктуры

Бизнес-логика не должна зависеть от баз данных, файловых систем, сетевых вызовов. Используйте репозитории и сервисы-обертки.

public interface IUserRepository
{
    User GetById(int id);
    void Save(User user);
}

public class UserService
{
    private readonly IUserRepository _repository;
    
    public UserService(IUserRepository repository)
    {
        _repository = repository;
    }
    
    public void UpdateUserName(int userId, string newName)
    {
        var user = _repository.GetById(userId);
        user.Name = newName;
        _repository.Save(user);
    }
}

4. Предпочтение композиции над наследованием

Наследование создает жесткую связь между классами, что усложняет тестирование. Композиция более гибка.

// Вместо наследования
public class AdvancedLogger : FileLogger
{
    public void LogWithTimestamp(string message)
    {
        // Зависит от реализации FileLogger
    }
}

// Используйте композицию
public class AdvancedLogger
{
    private readonly ILogger _logger;
    
    public AdvancedLogger(ILogger logger)
    {
        _logger = logger;
    }
    
    public void LogWithTimestamp(string message)
    {
        _logger.Log($"[{DateTime.Now}] {message}");
    }
}

5. Чистые функции для сложной логики

Чистые функции (pure functions) всегда возвращают одинаковый результат для одинаковых входных данных и не имеют побочных эффектов. Их легко тестировать.

// Чистая функция - легко тестировать
public static decimal CalculateDiscount(decimal amount, int loyaltyPoints)
{
    decimal baseDiscount = amount * 0.1m;
    decimal loyaltyBonus = loyaltyPoints * 0.01m;
    return Math.Min(baseDiscount + loyaltyBonus, amount * 0.3m);
}

// В отличие от метода с побочными эффектами
public decimal CalculateAndApplyDiscount(Order order)
{
    decimal discount = // ... расчет
    order.ApplyDiscount(discount); // Побочный эффект
    _database.Save(order); // Еще один побочный эффект
    return discount;
}

6. Использование паттерна "Фабрика" для сложных объектов

Создание сложных объектов лучше выносить в фабрики, которые можно подменить в тестах.

public interface IReportFactory
{
    Report CreateMonthlyReport(DateTime month, DataSource data);
}

public class ReportGenerator
{
    private readonly IReportFactory _factory;
    
    public Report Generate(DateTime month)
    {
        var data = LoadData(month);
        return _factory.CreateMonthlyReport(month, data);
    }
}

7. Ограничение использования статических методов и синглтонов

Статические классы и синглтоны создают скрытые зависимости, которые сложно подменить в тестах.

// ПЛОХО: статический вызов
public class PaymentService
{
    public void ProcessPayment(Payment payment)
    {
        Logger.Log($"Processing payment: {payment.Id}"); // Скрытая зависимость
        // ...
    }
}

// ХОРОШО: явная зависимость
public class PaymentService
{
    private readonly ILogger _logger;
    
    public PaymentService(ILogger logger)
    {
        _logger = logger;
    }
    
    public void ProcessPayment(Payment payment)
    {
        _logger.Log($"Processing payment: {payment.Id}");
        // ...
    }
}

Практические рекомендации для C#

  • Тестируйте поведение, а не реализацию — тесты не должны ломаться при рефакторинге
  • Используйте Moq, NSubstitute или FakeItEasy для создания заглушек
  • Применяйте FluentAssertions для читаемых проверок
  • Разделяйте тесты на Arrange-Act-Assert для четкой структуры
  • Избегайте тестов, зависящих от порядка выполнения

Легко тестируемый код — это, по сути, хорошо спроектированный код. Если код сложно покрыть тестами, это часто указывает на проблемы с архитектурой, которые стоит исправить для общего улучшения качества приложения.