Статья подготовлена в рамках набора на специализацию «C# — разработчик».
1. Недостаточное покрытие
Иногда из‑за нашей лени, невнимательности или чего‑либо ещё получается неполный охват тестами всех важных сценариев, крайних случаев и потенциальных ошибок. Представим, что у нас есть очень простой класс Calculator, который умеет делать сложение:
public int Add(int a, int b) { return a + b; }
Порой, когда на работе заставляют чтобы код сопровождался юнит‑тестами, может получиться класс, содержащий всего один тест:
[Test] public void Add_WhenCalled_ReturnsSum() { // Arrange var a = 40; var b = 2; var calculator = new Calculator(); // Act var result = calculator.Add(a,b); // Assert Assert.AreEqual(42, result); }
Тестирует ли код выше метод Add? Да, тестирует и даже гарантирует правильность сложения чисел 40 и 2. Однако, ни отрицательные числа, ни большие числа, сумма которых выходит за пределы размера int, ни сложение с нулем здесь не проверены.
Как сделать правильно?
Ещё до написания кода желательно вместе с аналитиком/тестировщиком/другими разработчиками обсудить все возможные корнер‑кейсы (крайние случаи) и то, как код должен реагировать при встрече с ними.
2. Переизбыток тестов
В противоположность к первому пункту можно увлечься и насоздавать много излишних тестов для тривиального или простого кода, что приведет к увеличению нагрузки на поддержку и замедлению выполнения тестов.
[Test] public void Add_WithFortyAndTwo_ReturnsFortyTwo() { // ... } [Test] public void Add_WithOneAndOne_ReturnsTwo() { // ... } [Test] public void Add_WithTenAndTen_ReturnsTwenty() { // ... } [Test] public void Add_WithZeroFirst_ReturnsSum() { // ... } [Test] public void Add_WithZeroSecond_ReturnsSum() { // ... }
Как сделать правильно?
Совет по составлению списка проверок с другими людьми здесь так же актуален. Плюс, порой можно воспользоваться передачей параметров в тестовый метод, что сокращает размер файла с тестами и их чтение. Еще более продвинутый вариант — использовать генераторы данных, например, Bogus. Ну и самый хардкорный вариант — использование pairwise testing.
3. Нетестируемый код
После работы в Лаборатории Касперского, где код обкладывался кучами разных тестов, я наиболее явно ощутил весь смысл слова «тестопригодность», когда встретил код, который мне пришлось несколько дней рефакторить, чтобы добавить для него юнит‑тесты. Один из примеров как сделать код нетестопригодным — использовать другие классы напрямую.
public class Calculator { private readonly ILogger _logger; public Calculator() { _logger = new Logger(); } public int Add(int a, int b) { _logger.Log("Add method called."); return a + b; } }
Ну и соответственно, чтобы сделать его тестопригодным, надо сделать инверсию зависимости, а если по‑русски, то заменить зависимость от класса на зависимость от интерфейса:
public class Calculator { private readonly ILogger _logger; public Calculator(ILogger logger) { _logger = logger; } public int Add(int a, int b) { _logger.Log("Add method called."); return a + b; } }
Как сделать правильно?
Не завязываться на конкретные реализации и как можно скорее убирать такие связи, если они есть — станет легче не только писать тесты, но и просто поддерживать код.
4. Игнорирование или пропуск тестов
Если бы сам не столкнулся с подобным в нескольких компаниях, то мне бы и в голову не пришло, что тесты можно (а порой даже нужно) игнорировать.
Как сделать правильно?
Назначить ответственного человека, либо самому следить за тем, чтобы игнорированием тестов не злоупотребляли, а использовали только когда необходимо. Например, когда идет крупномасштабное внедрение изменений — обновление кодстайла, сторонней или своей библиотеки.
5. Тестирование реализации
Довольно стандартная ловушка для новичков в юнит‑тестировании — написать сначала нужный код и потом на основе этого кода писать тесты. Причем тесты пишутся так, чтобы покрыть логику этого написанного кода. Проблема такого подхода — то, что не было написано, не будет и протестировано. Например, если при добавлении в класс Calculator метода Divide мы не учтем в коде проверку деления на ноль, то и при написании тестов по уже существующему коду вероятность написать тест деления на ноль исчезающе мала.
Как сделать правильно?
Вспоминаем совет к первым двум пунктам — составлять тест‑кейсы ДО написания кода и опираться в них на требуемую бизнес‑логику, а не на уже реализованную функциональность. Хотя, сразу оговорюсь, что для написания тестов к уже существующему коду, по которому никаких зафиксированных бизнес‑требований нет и никто их не знает/не помнит, подход на основе реализации вполне подходит. Но только для целей регрессионного тестирования.
6. Хрупкие тесты
Предположим, что мы написали класс А, который реализует необходимую нам бизнес‑логику. Затем, в процессе рефакторинга, мы вынесли из класса А два вспомогательных класса — В и С. Нужно ли писать тесты на все три класса? Конечно, это зависит от логики, которая была вынесена во вспомогательные классы и того, будет ли она использоваться где‑то ещё помимо класса А, однако, в 99% случаев писать тесты на классы В и С не нужно.
Как сделать правильно?
Рискуя набить оскомину, повторюсь, надо тестировать не написанный код, а бизнес‑логику, которая реализуется этим кодом.
7. Отсутствие организации тестов
Видел я и такие проекты, гды пытались внедрить юнит‑тесты, однако, все они лежали в корне тестового проекта и было тяжело разобраться есть ли уже нужные тебе тесты или нет. И вместо того, чтобы разбираться в этом бардаке и искать нужный класс, люди просто создавали ещё один, куда писали свои тесты. Хаос в таком случае только увеличивался.
Как сделать правильно?
Надо договориться о том, как будут организованы тесты в вашей компании/команде. Один из наиболее простых и распостраненных подходов — полностью копировать структуру основного проекта, добавляя постфикс «Tests». То есть, если был проект CalculationSolution и в нем был путь Calculations/Calculators/, по которому лежал файл Calculator.cs, для которого мы хотим добавить юнит‑тесты, то юнит‑тесты должны быть в проекте CalculationSolution.Tests по пути Calculations/Calculators/CalculatorTests.cs.
8. Божественные тесты
Как в процессе программирования может появиться god object — класс, который делает все и вся, так и при написании тестов могут получаться тесты, в которых проверяется не что‑то одно, а сразу штук 10 разных аспектов. Да, такая «денормализация» тестов порой имеет место быть в end‑to‑end, UI или интеграционных тестах в целях экономии ресурсов (в т.ч. времени выполнения), однако, юнит‑тесты должны проходить очень быстро и нет смысла усложнять себе разбор упавших тестов ради экономии пары миллисекунд.
Как сделать правильно?
Следить за тем, чтобы один тест тестировал только один аспект бизнес‑логики.
9. Недостаточная обработка ошибок
Соблазн протестировать happy path и, возможно, парочку самых простых в тестировании ошибок может привести к тому, что непойманные на этапе автоматизированного тестирования ошибки приведут к проблемам в продакшн среде и цена этой ошибки будет намного выше. Отличие этого пункта от первого в том, что, если в первом пункте было наглядно видно маленькое количество тестов, их практически не было, то здесь тесты уже есть и их даже может быть много, однако, они могут быть направлены на количество, а не на качество.
Как сделать правильно?
Опять же, составлять список тестов заранее, плюс, можно добавить в чеклист ревьюера пункт о том, что все тест‑кейсы должны быть реализованы.
10. Смешивание юнит‑тестов с другими видами тестов
Как я писал выше, юнит‑тесты обычно проходят очень быстро, так как не требуют сложной подготовки, подтягивания зависимостей и прочего. Остальные виды тестов уже несколько более продвинутые и более ресурсоемкие. Поэтому смешивание всех тестов в одну кучу является не очень хорошим вариантом.
Как сделать правильно?
Правильным будет разделять мух от котлет. Сделать это можно, например, разнеся тесты по разным проектам или используя идентифицирующие атрибуты. Далее с помощью этих атрибутов можно настроить так, чтобы ни один коммит не попадал ни в одну ветку до тех пор, пока все юнит‑тесты, связанные с этим кодом, не пройдут успешно. Также, можно выделить под разные виды тестов разные виртуальные машины и/или стратегии запуска этих тестов.
Хороший C#-разработчик — это не про знание синтаксиса, а про умение работать с архитектурой, зависимостями и тестами. Курс «C#-разработчик» как раз про это — про переход от «пишу код» к «пишу инженерные решения».

Если задача — ускорить рост в карьере, закрыть пробелы в знаниях или перейти в новую роль, важно выбрать обучение, которое напрямую помогает это сделать. В каталоге OTUS курсы собраны именно под такие сценарии. ➡ [Перейти к каталогу]

