Конечная цель, которую следует иметь в виду при написании тестов, - это то, что кто-то, читающий ваш тест, должен немедленно понять, что утверждается.

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

Мы будем использовать Jest в качестве библиотеки тестирования для остальной части статьи, но основные выводы не зависят от этой библиотеки.

Окончательную версию исходного кода, представленного в этой статье, можно найти здесь: https://github.com/inato/tests-clarity-in-DDD

Упрощение создания объектов с помощью фабрик объектов

Вы должны всегда иметь возможность создать объект своего домена без указания каких-либо аргументов

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

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

Этот тест полностью соответствует ожиданиям, но упускает один важный момент, когда речь идет о удобочитаемости: в тесте должно быть видно только то, что соответствует ожиданиям теста.

Что раздражает в этом тесте, так это то, что если кто-то прочитает этот тест, читатель будет совершенно прав, если задаст вопрос:

Но подождите, эта стратегия набора персонала специфична для больниц Франции?

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

Для этого теста важны только начальная и окончательная численность персонала.

Снижение шума при тестировании с объектными фабриками

Мы представим hospitalFactory, вспомогательную функцию, способную создавать Hospital с частичными аргументами.

Для этого простого объекта домена написать фабрику очень просто:

  1. Все аргументы фабрики являются необязательными и имеют значения по умолчанию.
  2. Фабрика всегда возвращает связанный объект домена

Давайте перепишем предыдущий тест с нашей новой фабрикой:

Ожидания от этого теста не изменились, но результат стал более ясным:

Читая этот тест, мы знаем, что единственное, что имеет отношение к принудительному предположению, - это численность персонала больницы

Следовательно, если вы следуете принципам DDD в своей кодовой базе, хорошее правило - всегда включать фабрику с объектом домена.

Фабрики объектов: расширяемость vs простота использования

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

Мы ввели новый Entity в нашем домене - Doctor.

В следующих разделах важно понимать:

  • У каждого врача есть уникальный идентификатор, который позволяет идентифицировать его среди других врачей.
  • В больнице есть staff - список врачей.
  • Один врач среди персонала - особенный, он также является директором больницы.
  • В больнице всегда должен быть директор в штате, иначе творение бросит. Обратите внимание, что добавление конструктора не рекомендуется и не согласуется с принципами DDD, а используется здесь только для простоты. Для более выразительной обработки ошибок вы можете проверить следующую статью:


Повторное использование уроков из раздела 1: doctorFactory

Поскольку мы уже поняли преимущества объектных фабрик, мы проведем рефакторинг hospitalFactory и представим doctorFactory:

doctorFactory довольно прост и следует тем же принципам, что и в разделе 1. hospitalFactory был расширен для поддержки новых атрибутов и попытки построить действующую больницу по умолчанию (путем назначения директора в штат, если персонал undefined)

Вы можете увидеть, как предприятия со временем будут получать прибыль: doctorFactory помогает тестировать бизнес-логику на Doctors, но также помогает создавать Hospital объектов.

Использование фабрик для проверки бизнес-логики около Hospital

Мы хотим проверить это:

  • Наем врача увеличивает штат больницы на 1
  • Наем врача, уже работающего в больнице, не увеличивает штат сотрудников на 1

Давайте попробуем протестировать метод hire агрегата Hospital, используя наши две фабрики:

Код довольно чистый в том смысле, что мы не вмешиваемся в атрибуты, не относящиеся к тесту (например, firstName, _21 _...), но в конечном итоге мы сталкиваемся с другой проблемой: из-за бизнес-логика вокруг директора, каждый раз, когда мы указываем персонал при создании больницы, нам нужно убедиться, что директор входит в штат, иначе создание будет отброшено.

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

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

Специализированные фабрики помогают снизить нагрузку на создание

Имея в виду те же цели (например, создание объекта должно быть простым, а аргументы - необязательными), мы создадим специализированную фабрику, чтобы иметь возможность создать больницу с конкретным врачом в штате не вмешиваясь в концепцию director.

Основными характеристиками этой специализированной фабрики являются:

  • Эта фабрика берет на себя ответственность за то, чтобы director был включен в staff. Вы даже можете использовать эту фабрику с doctor: null, чтобы включить директора только в начальный состав.
  • Эта фабрика возвращается к общему hospitalFactory, чтобы предоставить другие значения по умолчанию.

Задача этой фабрики - «Дайте мне врача, и я верну действующую больницу с этим доктором в штате».
Использование этой специализированной фабрики превратит наши тесты в:

Вы можете видеть, что нигде в этих тестах не присутствует концепция director, и что все нерелевантные переменные были удалены.

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

Подводя итог, фабрики объектов помогают нам решить две проблемы:

  1. Указание только того, что имеет отношение к контексту теста
  2. Упрощение создания объекта из нашего домена, даже если базовая логика, встроенная в этот объект, сложна

Снижение фиксированной стоимости тестирования сценариев использования с фабриками

Теперь, когда создание объектов из нашего домена стало проще благодаря нашим фабрикам, мы заинтересованы в тестировании варианта использования из нашего домена, а именно 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/