Ответ
Чтобы код был легко тестируемым, нужно проектировать его с учётом слабой связанности и явных зависимостей. Вот ключевые принципы и примеры:
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-окружения. И тогда жить станет проще, а тесты — предсказуемее. Ну, как минимум, начнёшь меньше материться при их запуске.