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

Ответ

Чтобы код был легко тестируемым, нужно проектировать его с учётом слабой связанности и явных зависимостей. Вот ключевые принципы и примеры:

1. Принцип единой ответственности (Single Responsibility): Класс должен решать одну задачу. Это упрощает написание тестов, так как нужно проверять меньше сценариев.

2. Внедрение зависимостей (Dependency Injection): Зависимости должны передаваться извне (через конструктор или свойства), а не создаваться внутри класса. Это позволяет подменять реальные реализации тестовыми заглушками (моками).

Пример плохого кода:

public class OrderProcessor
{
    private readonly EmailService _emailService;

    public OrderProcessor()
    {
        // Зависимость жёстко зашита внутри класса
        _emailService = new EmailService();
    }
    public void Process(Order order) { /* ... */ }
}

Пример хорошего, тестируемого кода:

public interface INotificationService
{
    void SendConfirmation(Order order);
}

public class OrderProcessor
{
    private readonly INotificationService _notificationService;

    // Зависимость инжектируется через конструктор
    public OrderProcessor(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public void Process(Order order)
    {
        // Логика обработки заказа...
        _notificationService.SendConfirmation(order);
    }
}

Теперь в юнит-тесте можно легко подменить INotificationService моком и проверить, был ли вызван метод SendConfirmation.

3. Предпочитайте чистые функции: Функции, которые для одних и тех же входных данных всегда возвращают одинаковый результат и не имеют побочных эффектов (не изменяют глобальное состояние), тестировать проще всего.

4. Избегайте статических методов и синглтонов: Они создают скрытые зависимости, которые сложно подменить в изоляции, что приводит к хрупким тестам, зависящим от глобального состояния.

Ответ 18+ 🔞

Давай разберём, как писать код, который не превратится в ад при первом же тесте. А то ведь бывает — открываешь класс, а там такая каша, что хоть святых выноси. И начинаешь писать тест, а он упирается в базу данных, файловую систему и ещё какую-нибудь левую службу рассылки, которую в тестовом окружении и близко нет. Короче, пиздец, а не тестирование.

Вот смотри, чтобы такого не было, нужно держать в голове несколько простых, но ёбаных правил.

Первое — принцип единой ответственности. Это когда класс делает что-то одно, а не пытается быть швейцарским ножом. Представь мужика, который и плитку кладёт, и сантехнику ставит, и проводку делает. Вроде круто, но когда он накосячит с проводкой и всё к хуям замкнёт — кто виноват? А хуй разберёшь. Так и с классом: если он делает одно дело, то и тестировать его — раз плюнуть. Проверил одну фичу и спи спокойно.

Второе — внедрение зависимостей. Это вообще святое. Нельзя внутри класса тупо создавать сервисы через new. Потому что потом прижмёт — и как ты подменишь этот сервис на заглушку? Никак, только ебись с реальной почтовой рассылкой в тестах.

Вот смотри на пример говнокода:

public class OrderProcessor
{
    private readonly EmailService _emailService;

    public OrderProcessor()
    {
        // Зависимость жёстко зашита внутри класса
        _emailService = new EmailService();
    }
    public void Process(Order order) { /* ... */ }
}

Что здесь происходит? Класс сам себе создаёт EmailService. И теперь он намертво привязан к этой конкретной реализации. Хочешь протестировать логику без реальной отправки писем? Да хуй там! Придётся или молиться, чтобы тестовая почта не сломалась, или вообще отключить эту часть кода. Кошмар.

А теперь — нормальный вариант:

public interface INotificationService
{
    void SendConfirmation(Order order);
}

public class OrderProcessor
{
    private readonly INotificationService _notificationService;

    // Зависимость инжектируется через конструктор
    public OrderProcessor(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public void Process(Order order)
    {
        // Логика обработки заказа...
        _notificationService.SendConfirmation(order);
    }
}

Видишь разницу? Теперь мы говорим: «Слушай, OrderProcessor, тебе для работы нужен какой-то сервис уведомлений. Какой именно — нам похуй, главное чтобы у него был метод SendConfirmation». И в тесте мы спокойно можем подсунуть ему заглушку, которая ничего не отправляет, но запоминает, вызывался ли метод. И проверить это. Красота!

Третье — чистые функции. Это вообще песня. Функция, которая получает одни данные, возвращает другие и не трогает ничего вокруг — это мечта тестировщика. Кинул на вход 2 и 3 — получил 5. Всё. Никаких сюрпризов, никаких побочных эффектов. Такие штуки тестировать — одно удовольствие, даже настроение поднимается.

Четвёртое — долой статику и синглтоны. О, это отдельная боль. Кажется, ну вот же удобно — SomeService.Instance.DoSomething(). А потом начинаешь писать тесты и понимаешь, что этот инстанс уже где-то использовался, состояние у него грязное, а ещё он тянет за собой пол-системы. И всё, тесты начинают зависеть друг от друга, падать в случайном порядке и доводить тебя до белого каления. Проще сразу выкинуть эту привычку в пизду. Зависимости должны быть явными и передаваться извне.

Вот и весь секрет. Пиши код так, чтобы его можно было разобрать на части и проверить каждую по отдельности, не вызывая духов из production-окружения. И тогда жить станет проще, а тесты — предсказуемее. Ну, как минимум, начнёшь меньше материться при их запуске.