Pull to refresh

Анатомия юнит тестирования

IT systems testingDevelopment Management
Юнит тесты — обязательная часть моих проектов. Это база, к которой добавляются другие виды тестов. В статье Тестирование и экономика проекта я рассказал почему тестирование выгодно для экономики проекта и показал, что юнит тестирование лидирует с экономической точки зрения. В комментариях было высказано мнение, что тестирование требует больших усилий, и даже юнит тестирование неприемлемо из-за этого. Одной из причин этого является неопытность команды в тестировании. Чтобы написать первую тысячу тестов команда тратит много времени, пробуя и анализируя различные подходы.

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

Я определяю юнит тестирования как тестирование одного продакш юнита в полностью контролируемом окружении.

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

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

О наследование


Постарайтесь не применять наследование. Вместо него используйте композицию зависимостей. Часто наследование применяют для реализации принципа DRY (don’t repeat yourself), вынося общий код в родителя, но тем самым нарушая принцип KISS (keep it simple stupid), увеличивая сложность юнитов.

AAA (Arrange, Act, Assert) паттерн


Если посмотреть на юнит тест, то для большинства можно четко выделить 3 части кода:

Arrange (настройка) — в этом блоке кода мы настраиваем тестовое окружение тестируемого юнита;
Act — выполнение или вызов тестируемого сценария;
Assert — проверка того, что тестируемый вызов ведет себя определенным образом.
Этот паттерн улучшает структуру кода и его читабельность, однако начинать писать тест нужно всегда с элемента Act.

Driven approach


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

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

С чего мы начинаем разработку конкретного функционала? — с требований бизнеса, которые типично выглядят так: “Пользователь с любой ролью должен иметь возможность создать запись, таким образом, он выполнит такую то бизнес операцию”.

Используя driven approach первое что мы должны сделать —

  • Это создать место в UI слое, где пользователь может создать запись, скажем, страницу в приложении, на которой будет кнопка “Создать запись”. Почему мы это сделали? — потому что это требует бизнес история.
  • Кнопка “Создать запись” будет требовать реализации обработчика click события.
  • Обработчик события будет требовать реализации создания записи в терминах слоя бизнес логики.
  • В случае клиент-серверной архитектуры, клиент будет обращаться к некоторому end point на стороне сервера для создания этой записи.
  • Сервер, в свою очередь, может работать с базой данных, где такая запись должна быть создана в отдельной таблице.

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

Данный подход позволяет небольшими шагами реализовывать сложные бизнес истории, оставаясь все время сфокусированным только на нужном функционале, и избегать over engineering.

AAS (Act, Assert, Setup) паттерн


AAS — этот тот же AAA паттерн, но с измененным порядком частей, отсортированных с учетом Driven approach и переименованной Arrange частью в Setup, чтобы отличать их по названию.

Первое, что мы делаем, при создании теста — мы создаем Act. Обычно это создание экземпляра класса тестируемого юнита и вызов его функции. С одной стороны — это самый простой шаг, а с другой это то, что диктует нам бизнес история.

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

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

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

Так что именно сейчас, когда мы будем писать последнюю часть теста, мы можем остановиться и продумать, как наш юнит будет работать и какое runtime окружение ему для этого нужно. И здесь мы переходим более подробно к “Контролируемому окружению” и дизайну юнита.

Принципы SOLID


Из принципа SOLID, с точки зрения юнит тестирования очень важны 2 принципа:

Single responsibility principle — позволяет снизить количество тест кейсов для юнита. В среднем на юнит должно приходиться от 1 до 9 тест кейсов. Это очень хороший индикатор качества юнита — если тест кейсов больше или хочется их сгруппировать, то вам точно нужно разделить его на два и больше независимых юнитов.

Dependency inversion principle — позволяет легко создавать и управлять сложнейшими окружениями для тестирования через IoC контейнеры. В соответствии с данным принципом, юнит должен зависеть от абстракций, что позволяет передавать ему любые реализации его зависимостей. В том числе, и не продакшен реализации, созданные специально для его тестирования. Эти реализации не имеют в себе никакой бизнес логики и созданы не только под конкретный тестируемый юнит, но и под конкретный сценарий его тестирования. Обычно они создаются с помощью одной из библиотек для mock объектов, такой как moq.

IoC контейнеры позволяют автоматически создавать экземпляр тестируемого юнита и экземпляры его зависимостей, сразу реализованные как mock объекты. Использование такого IoC контейнера очень важный шаг к снижению стоимости поддержания кода и его дружелюбности к автоматическому рефакторингу.

Качество кода


Кстати, несколько слов о качестве кода тестов и продакшн. Самым качественным кодом должен быть код тестов. Причина этому одна — это его размер. На 1 строку продакшн кода в среднем приходиться 2-3 строки тестового кода, то есть его в 2-3 раза больше чем продакшн кода. В этих условиях он должен хорошо читаться, быть структурированным, иметь хорошую типизацию и быть очень дружелюбным к инструментам автоматического рефакторинга. Это цели, которые достойны отдельных мероприятий и усилий.

Однотипность тестирования


Много приложения реализовано в распределенной и модульной архитектуре, где разные части написаны на различных языках, скажем, клиент-серверные приложения, где клиент написан под веб на typescript и сервер написанный на c#. Важной целью для таких проектов будет приведение тестов для любой части, независимо от языка к единому подходу. Это значит, что все тесты на проекте используют AAA или AAS подход. Все тесты используют mock библиотеки с похожим API. Все тесты используют IoC. И все тесты используют одинаковые метафоры. Это позволяет повысить переносимость удачных практик на разные части проекта, упростить адаптацию новых коллег (выучил раз и применяй везде).

Количество тестов для одного продакшн юнита


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

Моя команда создает клиент-серверные приложения, где мы используем angular на клиенте и .net core для серверной части. В следующей статье я хочу показать на примерах, как мы пишем юнит тесты под angular и с#. Как мы делаем их похожими, как располагаем в проектах, какие библиотеки применяем.
Tags:unit testingtestingpractices
Hubs: IT systems testing Development Management
Total votes 8: ↑6 and ↓2+4
Views7.6K
Comments Comments 21

Popular right now

Top of the last 24 hours