Конечная цель, которую следует иметь в виду при написании тестов, - это то, что кто-то, читающий ваш тест, должен немедленно понять, что утверждается.
В этой статье будут рассмотрены некоторые проблемы, часто возникающие при тестировании больших или сложных объектов или агрегатов, особенно в настройке Domain-Driven Design.
Мы будем использовать Jest в качестве библиотеки тестирования для остальной части статьи, но основные выводы не зависят от этой библиотеки.
Окончательную версию исходного кода, представленного в этой статье, можно найти здесь: https://github.com/inato/tests-clarity-in-DDD
Упрощение создания объектов с помощью фабрик объектов
Вы должны всегда иметь возможность создать объект своего домена без указания каких-либо аргументов
Допустим, часть вашего домена моделирует работу и функционирование больницы. Базовый первый набросок может выглядеть так:
Вы хотите написать тест, чтобы убедиться, что функция hireSomeone
эффективно увеличивает размер персонала на 1.
Первая попытка может выглядеть так:
Этот тест полностью соответствует ожиданиям, но упускает один важный момент, когда речь идет о удобочитаемости: в тесте должно быть видно только то, что соответствует ожиданиям теста.
Что раздражает в этом тесте, так это то, что если кто-то прочитает этот тест, читатель будет совершенно прав, если задаст вопрос:
Но подождите, эта стратегия набора персонала специфична для больниц Франции?
Мы могли бы представить себе область, в которой больницы должны сначала проверить, действительно ли им нужен новый врач, прежде чем подтверждать нового найма.
Тот факт, что мы явно создали французскую больницу, вносит шум в тест и усложняет его чтобы найти здесь то, что имеет отношение к делу.
Для этого теста важны только начальная и окончательная численность персонала.
Снижение шума при тестировании с объектными фабриками
Мы представим hospitalFactory
, вспомогательную функцию, способную создавать Hospital
с частичными аргументами.
Для этого простого объекта домена написать фабрику очень просто:
- Все аргументы фабрики являются необязательными и имеют значения по умолчанию.
- Фабрика всегда возвращает связанный объект домена
Давайте перепишем предыдущий тест с нашей новой фабрикой:
Ожидания от этого теста не изменились, но результат стал более ясным:
Читая этот тест, мы знаем, что единственное, что имеет отношение к принудительному предположению, - это численность персонала больницы
Следовательно, если вы следуете принципам DDD в своей кодовой базе, хорошее правило - всегда включать фабрику с объектом домена.
Фабрики объектов: расширяемость vs простота использования
Через несколько дней вы хотите уточнить свой домен вокруг Hospital
и хотите улучшить обзор персонала, работающего в больнице. Ваш усовершенствованный класс выглядит так:
Мы ввели новый Entity
в нашем домене - Doctor
.
В следующих разделах важно понимать:
- У каждого врача есть уникальный идентификатор, который позволяет идентифицировать его среди других врачей.
- В больнице есть
staff
- список врачей. - Один врач среди персонала - особенный, он также является директором больницы.
- В больнице всегда должен быть директор в штате, иначе творение бросит. Обратите внимание, что добавление конструктора не рекомендуется и не согласуется с принципами DDD, а используется здесь только для простоты. Для более выразительной обработки ошибок вы можете проверить следующую статью:
Повторное использование уроков из раздела 1: doctorFactory
Поскольку мы уже поняли преимущества объектных фабрик, мы проведем рефакторинг hospitalFactory
и представим doctorFactory
:
doctorFactory
довольно прост и следует тем же принципам, что и в разделе 1. hospitalFactory
был расширен для поддержки новых атрибутов и попытки построить действующую больницу по умолчанию (путем назначения директора в штат, если персонал undefined
)
Вы можете увидеть, как предприятия со временем будут получать прибыль: doctorFactory
помогает тестировать бизнес-логику на Doctor
s, но также помогает создавать Hospital
объектов.
Использование фабрик для проверки бизнес-логики около Hospital
Мы хотим проверить это:
- Наем врача увеличивает штат больницы на 1
- Наем врача, уже работающего в больнице, не увеличивает штат сотрудников на 1
Давайте попробуем протестировать метод hire
агрегата Hospital
, используя наши две фабрики:
Код довольно чистый в том смысле, что мы не вмешиваемся в атрибуты, не относящиеся к тесту (например, firstName
, _21 _...), но в конечном итоге мы сталкиваемся с другой проблемой: из-за бизнес-логика вокруг директора, каждый раз, когда мы указываем персонал при создании больницы, нам нужно убедиться, что директор входит в штат, иначе создание будет отброшено.
Это дополнительный шум, который мы хотели бы удалить, поскольку эти тесты не имеют ничего общего с директорами.
Поскольку наши объекты домена становятся все более и более сложными, создание их с помощью всего лишь нескольких аргументов становится проблемой
Специализированные фабрики помогают снизить нагрузку на создание
Имея в виду те же цели (например, создание объекта должно быть простым, а аргументы - необязательными), мы создадим специализированную фабрику, чтобы иметь возможность создать больницу с конкретным врачом в штате не вмешиваясь в концепцию director
.
Основными характеристиками этой специализированной фабрики являются:
- Эта фабрика берет на себя ответственность за то, чтобы
director
был включен вstaff
. Вы даже можете использовать эту фабрику сdoctor: null
, чтобы включить директора только в начальный состав. - Эта фабрика возвращается к общему
hospitalFactory
, чтобы предоставить другие значения по умолчанию.
Задача этой фабрики - «Дайте мне врача, и я верну действующую больницу с этим доктором в штате».
Использование этой специализированной фабрики превратит наши тесты в:
Вы можете видеть, что нигде в этих тестах не присутствует концепция director
, и что все нерелевантные переменные были удалены.
Комбинация фабрик общих объектов и специализированных фабрик очень эффективна и становится все более выгодной для вашей кодовой базы по мере того, как ваши объекты становятся все более и более вложенными.
Подводя итог, фабрики объектов помогают нам решить две проблемы:
- Указание только того, что имеет отношение к контексту теста
- Упрощение создания объекта из нашего домена, даже если базовая логика, встроенная в этот объект, сложна
Снижение фиксированной стоимости тестирования сценариев использования с фабриками
Теперь, когда создание объектов из нашего домена стало проще благодаря нашим фабрикам, мы заинтересованы в тестировании варианта использования из нашего домена, а именно replaceSomeoneInHospital
, который заменяет одного врача другим в штате больницы. (, например, если первый врач заболел).
Этот вариант использования принимает в качестве аргументов:
initialDoctor: Doctor
заменитьnewDoctor: Doctor
поставить взамен замененногоhospitalName: string
название больницы, в отношении которой производится замена (связанный объектHospital
будет найден прецедентом с использованиемhospitalRepository
)hospitalRepository: HospitalRepository
, отвечающий за поиск или хранениеHospital
объектов (с использованием баз данных, в памяти и т. Д.)
Чтобы излишне упростить нашу точку зрения, мы спроектируем наш вариант использования, чтобы вернуть объект Hospital
, равный обновленной больнице , если замена была выполнена успешно, и null
, если замена не может быть выполнена.
Мы хотим протестировать одно бизнес-правило для:
- Замена не должна увеличивать или уменьшать штат больницы.
Этот тест наивно выглядел бы так:
Это не так уж и плохо, поскольку мы не предоставляем много переменных, не относящихся к тесту, но у нас есть проблема, которая будет становиться все более раздражающей по мере тестирования новых предположений для этого варианта использования: нам пришлось просто настроить некоторые объекты. чтобы иметь возможность вызвать вариант использования.
Эти объекты, такие как hospitalRepository
или тот факт, что больница должна храниться в репозитории перед вызовом варианта использования, не имеют отношения к этому тесту и ухудшают удобочитаемость.
Фиксированная стоимость настройки объектов для варианта использования будет добавлять шум в каждом тесте этого варианта использования
Чтобы уменьшить нагрузку, связанную с вызовом варианта использования, мы реорганизуем эту настройку в функцию, которая сделает большую часть этого за нас и вернет упрощенную (например, карризованную) версию нашего варианта использования, который будет принимать только релевантные переменные в качестве входных данных.
Если вы посмотрите на размер теста в it()
скобках с помощником по настройке варианта использования и без него, то сможете понять, почему преимущества очень существенны, поскольку мы тестируем больше правил для нашего варианта использования.
Однако основные преимущества связаны с удобочитаемостью этого теста:
- Помощник по настройке позаботится о создании больницы с первоначальным врачом и хранении больницы.
- Помощник возвращает каррированную версию варианта использования, где нам нужно только предоставить соответствующие данные, здесь доктор, который заменит первоначального врача.
Окончательную версию исходного кода, представленного в этой статье, можно найти здесь: https://github.com/inato/tests-clarity-in-DDD
Открытие лекарств - это сложное, интеллектуально сложное и полезное дело: мы помогаем разрабатывать эффективные и безопасные лекарства от болезней, от которых страдают миллионы людей. Если вы хотите оказать огромное влияние, присоединяйтесь к нам! Https://angel.co/inato/jobs/