Skip to content

Instantly share code, notes, and snippets.

@jigi-33
Last active March 6, 2023 15:25
Show Gist options
  • Save jigi-33/25c4f2eefa04803f2d936d6377ea7b54 to your computer and use it in GitHub Desktop.
Save jigi-33/25c4f2eefa04803f2d936d6377ea7b54 to your computer and use it in GitHub Desktop.
Unit-тестирование: подходы и лучшие практики

Подходы и лучшие практики в UNIT-тестировании

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

Ниже упомянуты best practices & approaches of unit testing.

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

Продакшн юнит - обычно класс, но может быть и функция, и файл. Важно, чтобы юнит соответствовал принципал 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... и т.д. Это позволит повысить переносимость удачных практик на разные части проекта, упростить адаптацию новых коллег (выучил раз и применяй везде). Всем в команде нужно делать тесты похожими и похожим образом располагать их в проектах.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment