14 советов по написанию модульных тестов

Несомненно, покрытие приложения модульными тестами повышает его качество. Хорошие модульные тесты придают разработчикам уверенности при добавлении новых функций в приложение. Они помогают быстро находить ошибки в коде и исправлять их до того, как эти ошибки попадут в продакшн. Если же тесты неэффективны, это может значительно усложнить работу разработчиков, тем самым заставляя их тратить время на исправление самих тестов, вместо того, чтобы заниматься созданием новых функций в приложении.

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

Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.

Содержание

1. Тестируйте небольшие фрагменты кода изолированно

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

Чем больше логики вы пытаетесь охватить тестами, тем сложнее найти точную причину сбоя. Вполне нормально тестировать функции, зависящие от других классов, которые также покрыты модульными тестами. Если что-то сломается, связанные тесты тоже упадут, и вы сможете быстро определить, где проблема. Но когда не проходят несколько тестов, найти причину становится сложнее.

2. Arrange, Act, Assert

AAA (Arrange, Act, Assert) – это общий подход к написанию более читабельных модульных тестов.

На первом этапе вы подготавливаете все для тестирования. Здесь вы задаете переменные, создаете объекты и выполняете все остальные настройки, необходимые для запуска теста. На этом этапе вы также определяете ожидаемый результат.

Далее вы вызываете тестируемую функцию и сохраняете её результаты. После этого вы можете проанализировать эти результаты.

И наконец, вы проверяете правильность работы функции. Цель состоит в том, чтобы убедиться, что полученный результат соответствует ожидаемому.

3. Делайте тесты короткими

Короткие тесты гораздо легче читать и понимать. Поскольку мы тестируем один фрагмент логики за раз, тесты не должны быть длиннее нескольких строк кода.

Модульные тесты не сильно отличаются от обычного кода, поэтому к ним применяется принцип DRY (don’t repeat yourself – “не повторяйся”). Не забывайте, что вам придется поддерживать эти тесты в будущем.

4. Делайте тесты простыми

Избегайте сложной логики в тестах.

Заманчиво написать один кусок кода, который будет работать для разных тестовых сценариев. Это может показаться хорошей идеей, так вроде бы соответствует принципу DRY, который гласит, что не нужно дублировать код. Однако, когда что-то пойдет не так и тест не пройдет, вам придется разбираться и отлаживать не только сам тест, но и этот универсальный кусок кода. Это может быть сложно, так как нужно разобраться, где именно возникла проблема.

По мере усложнения вашего проекта универсальный код может перестать работать для всех случаев. Кроме того, сам тест тоже может усложниться, чтобы соответствовать всем тестовым сценариям.

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

5. Сперва тестируйте “счастливые” пути

Когда вы тестируете новую функциональность, лучше всего начать с проверки “счастливого” пути — идеального сценария, где всё работает как задумано.

  • Это самые простые в написании тесты.
  • Если такие тесты проходят успешно, это подтверждает, что базовая настройка для тестирования работает правильно.
  • Они демонстрируют, как именно должен работать функционал приложения.

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

6. Тестируйте крайние значения

Теперь, когда вы уже протестировали сценарии “счастливого” пути, пришло время протестировать редкие случаи: неправильный ввод, отсутствие аргументов, пустые данные, исключения в вызываемых функциях и т. д. Инструменты анализа покрытия кода могут помочь найти ветви кода, которые еще не протестированы. Не стоит стремиться к 100-процентному тестовому покрытию.

Некоторые предпочитают, чтобы модульные тесты были независимыми от тестируемого алгоритма. Это означает, что они проверяют только конечный результат работы кода, независимо от его внутренней логики. Однако другие считают полезным писать тесты, учитывающие особенности алгоритма, такие как крайние случаи или возможные ошибки. Например, при тестировании алгоритма определения принадлежности точки треугольнику, крайними случаями могут быть точки, находящиеся на вершинах или ребрах треугольника. Такие тесты помогают обнаруживать проблемы в алгоритме, которые могут быть упущены.

7. Пишите тесты, прежде чем исправлять ошибки

После того, как вы нашли неправильно работающий код, подумайте о написании теста, который воспроизводит эту ошибку. Исправить её, отлаживая тест отдельно от остального кода приложения, будет намного быстрее.

Это будет надежный регрессионный тест, который поможет вам обнаружить эту ошибку в будущем.

8. Подготовьте тесты к выполнению

Модульные тесты должны работать на каждой машине. Ваша команда должна запускать их несколько раз в день. Они будут выполняться как во время локальных сборок, так и в CI. Вам необходимо, чтобы они выполнялись быстро.

Не забудьте применить моки для всех внешних зависимостей, которые могут замедлить работу, например вызовы API, базы данных или доступ к файловой системе. Избегайте в тестах thread sleep и timeouts. Даже если вы тестируете timeouts, старайтесь делать их очень короткими, всего несколько миллисекунд.

9. Делайте тесты независимыми

Ваши тесты не должны изменять что-либо за пределами тестируемой функции или оставлять побочные эффекты. Если ваши тесты проходят только тогда, когда выполняются в определенном порядке, значит, что-то не так либо с ними, либо с тестируемым кодом. Тесты должны быть независимы друг от друга.

Если вам нужна какая-то сложная повторяющаяся конфигурация к тестам, используйте механизмы настройки (setup) и завершения (teardown), предоставленные фреймворками. Они гарантированно выполняются перед каждым тестом или после всего набора тестов. Таким образом, ваш тест будет работать при запуске как отдельно, так и в составе всего набора тестов. Порядок выполнения не будет иметь значения.

10. Пишите детерминированные тесты

Если тест проходит, он должен проходить всегда, а если падает – тоже всегда. Время суток, расположение звезд или уровень прилива не должны влиять на это.

Вот почему полагаться на внешние зависимости – не самая лучшая идея. API может не работать или кто-то решит запустить тесты в полночь. При написании тестируемого кода все, что находится вне вашего контроля, должно рассматриваться как зависимость.

Например, если в вашем коде используется Date.now(), то результаты выполнения кода могут изменяться в зависимости от текущего времени. Поэтому ваши тесты должны быть построены таким образом, чтобы они могли имитировать работу этих внешних зависимостей и всегда давать надежные результаты.

11. Используйте понятные названия

Первое, что вы видите, когда тест падает, – это его название. Оно должно содержать достаточно информации, чтобы понять, что именно не получилось.

Не бойтесь длинных названий. Например, для функции подсчета суммы название теста it('должен возвращать 0 для пустой корзины') гораздо лучше, чем it('работает для 0') или it('пустая корзина').

12. Тестируйте по одному требованию за раз

Не тестируйте весь метод сразу. При тестировании каждого требования отдельно код становится намного проще. Таким образом, тест будет менее раздутым, более легким для чтения и более простым в поддержке.

Если требования меняются, внесите изменения в соответствующие тесты. Вам не нужно просматривать все тесты и проверять, что из них было затронуто.

13. Выбирайте более точные утверждения (asserts)

Тестовые фреймворки предоставляют разные методы проверки результатов тестов и получения сообщений об ошибках, когда тест не проходит. Это помогает лучше понять, что именно пошло не так.

Например,

expect(result === expected).toBeTruthy();

выполнится с ошибкой

expect(received).toBeTruthy()Received: false

в то время как

expect(result).toBe(expected);

даст больше информации о том, что именно не сработало:

expect(received).toBe(expected) // Object.is equalityExpected: "John Doe"Received: "JohnDoe"

14. Запускайте тесты автоматически

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

Чтобы не допустить добавления кода с неудачными тестами в репозиторий, рассмотрите возможность запуска тестов перед отправкой изменений в git. Для JavaScript- и TypeScript-проектов это можно настроить с помощью husky.

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

Заключение

Если вы не пишете модульные тесты, вы действительно не можете рефакторить свой код без того, чтобы случайно не сломать что-нибудь. Код становится сложнее поддерживать, и даже исправление существующих ошибок становится сложной задачей. Хорошие модульные тесты дадут вам возможность исправить ошибки до того, как ваш код попадет в репозиторий.

Чтобы быть хорошим в чем-то, всегда нужна практика.

Перевод статьи «How to Write Good Unit Tests: 14 Tips».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *