Юнит тесты — обязательная часть моих проектов. Это база, к которой добавляются другие виды тестов. В статье Тестирование и экономика проекта я рассказал почему тестирование выгодно для экономики проекта и показал, что юнит тестирование лидирует с экономической точки зрения. В комментариях было высказано мнение, что тестирование требует больших усилий, и даже юнит тестирование неприемлемо из-за этого. Одной из причин этого является неопытность команды в тестировании. Чтобы написать первую тысячу тестов команда тратит много времени, пробуя и анализируя различные подходы.
В этой статье я расскажу о лучших практиках, к которым я пришел за более чем 10 лет тестирования различных проектов. Эти практики позволят начать юнит тестирование без заметного снижения производительности программистов.
Я определяю юнит тестирования как тестирование одного продакш юнита в полностью контролируемом окружении.
Продакшн юнит — это обычно класс, но может быть также и функция, и файл.
Важно, чтобы юнит соответствовал принципал SOLID, в этом случае юнит тесты будут лаконичными. Юниты узкоспециализированны и очень хорошо выполняют одну конкретную задачу, для которой они созданы. В большинстве случаев юниты взаимодействуют друг с другом, делегируя выполнение специализированных задач.
Полностью контролируемое окружение — это окружение имитирующие среду, в которой юнит работает в программе, но полностью открытое для тестирования и настройки. Поведение окружения задается для конкретного тестового кейса через лаконичный API и любое поведение вне этого кейса для него не определено. В этом окружении не должно быть других продакшн юнитов, иначе возрастает сложность тестов и из юнит тестирования мы переходим к интеграционному тестированию.
Постарайтесь не применять наследование. Вместо него используйте композицию зависимостей. Часто наследование применяют для реализации принципа DRY (don’t repeat yourself), вынося общий код в родителя, но тем самым нарушая принцип KISS (keep it simple stupid), увеличивая сложность юнитов.
Если посмотреть на юнит тест, то для большинства можно четко выделить 3 части кода:
Arrange (настройка) — в этом блоке кода мы настраиваем тестовое окружение тестируемого юнита;
Act — выполнение или вызов тестируемого сценария;
Assert — проверка того, что тестируемый вызов ведет себя определенным образом.
Этот паттерн улучшает структуру кода и его читабельность, однако начинать писать тест нужно всегда с элемента Act.
Прежде чем продолжить рассмотрение структуры теста, я хотел бы рассказать немного о подходе, который я называю Driven Approach. К сожалению, я не могу перевести это лаконично на русский язык. Поэтому просто расскажу, что за этим стоит, и может быть, вы поможете с лаконичным переводом.
Суть в том, что код, который вы пишите, должен иметь причину своего существования. Важно, чтобы причина была существующей, а не предполагаемой, и эта причина должна иметь в конечном итоге связь с бизнес историей.
С чего мы начинаем разработку конкретного функционала? — с требований бизнеса, которые типично выглядят так: “Пользователь с любой ролью должен иметь возможность создать запись, таким образом, он выполнит такую то бизнес операцию”.
Используя driven approach первое что мы должны сделать —
Как видите, существование каждого этапа реализации обусловлено существованием предыдущего этапа. И каждый этап, даже самый последний — создание записи в таблице базы данных имеет связь с изначальной бизнес историей. Каждый этап реализуется только в том объеме, который нужен предыдущему этапу.
Данный подход позволяет небольшими шагами реализовывать сложные бизнес истории, оставаясь все время сфокусированным только на нужном функционале, и избегать over engineering.
AAS — этот тот же AAA паттерн, но с измененным порядком частей, отсортированных с учетом Driven approach и переименованной Arrange частью в Setup, чтобы отличать их по названию.
Первое, что мы делаем, при создании теста — мы создаем Act. Обычно это создание экземпляра класса тестируемого юнита и вызов его функции. С одной стороны — это самый простой шаг, а с другой это то, что диктует нам бизнес история.
Второе — мы проверяем что Act действует ожидаемо. Мы пишем Assert часть, где выражаем требуемые последствия Act, в том числе с точки зрения бизнес истории.
И вот, когда у нас готовы первые 2 части, мы можем остановиться и подумать, как наш юнит будет выполнять требуемые действия. Да, да, именно сейчас мы можем первый раз задуматься, как реализовать его.
Смотрите, сам вид Act и его сигнатура продиктованы предыдущим шагом, тут нам нечего изобретать. Как предыдущий шаг хочет вызывать наш юнит, так он и будет его вызывать. Ожидаемые действия тоже продиктованы предыдущим шагом и самой бизнес историей.
Так что именно сейчас, когда мы будем писать последнюю часть теста, мы можем остановиться и продумать, как наш юнит будет работать и какое runtime окружение ему для этого нужно. И здесь мы переходим более подробно к “Контролируемому окружению” и дизайну юнита.
Из принципа 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 и с#. Как мы делаем их похожими, как располагаем в проектах, какие библиотеки применяем.
В этой статье я расскажу о лучших практиках, к которым я пришел за более чем 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 и с#. Как мы делаем их похожими, как располагаем в проектах, какие библиотеки применяем.