Я не считаю себя экспертом DDD, но, как архитектор решений, стараюсь по возможности применять лучшие практики. Я знаю, что вокруг «за» и «против» нет (публичного) «стиля» сеттера в DDD, много споров, и я вижу обе стороны аргумента. Моя проблема в том, что я работаю в команде с широким разнообразием навыков, знаний и опыта, и это означает, что я не могу поверить, что каждый разработчик будет делать вещи «правильным» образом. Например, если наши доменные объекты спроектированы так, что изменения внутреннего состояния объекта выполняются методом, но предоставляют установщики открытых свойств, кто-то неизбежно установит свойство вместо вызова метода. Используйте этот пример:
public class MyClass
{
public Boolean IsPublished
{
get { return PublishDate != null; }
}
public DateTime? PublishDate { get; set; }
public void Publish()
{
if (IsPublished)
throw new InvalidOperationException("Already published.");
PublishDate = DateTime.Today;
Raise(new PublishedEvent());
}
}
Мое решение состояло в том, чтобы сделать установщики свойств частными, что возможно, потому что ORM, который мы используем для гидратации объектов, использует отражение, чтобы иметь возможность доступа к частным установщикам. Однако это создает проблему при попытке написания модульных тестов. Например, когда я хочу написать модульный тест, который проверяет требование, которое мы не можем повторно опубликовать, я должен указать, что объект уже был опубликован. Конечно, я могу сделать это, дважды позвонив в Publish, но тогда мой тест предполагает, что Publish реализован правильно для первого вызова. Это кажется немного вонючим.
Давайте сделаем сценарий немного более реальным с помощью следующего кода:
public class Document
{
public Document(String title)
{
if (String.IsNullOrWhiteSpace(title))
throw new ArgumentException("title");
Title = title;
}
public String ApprovedBy { get; private set; }
public DateTime? ApprovedOn { get; private set; }
public Boolean IsApproved { get; private set; }
public Boolean IsPublished { get; private set; }
public String PublishedBy { get; private set; }
public DateTime? PublishedOn { get; private set; }
public String Title { get; private set; }
public void Approve(String by)
{
if (IsApproved)
throw new InvalidOperationException("Already approved.");
ApprovedBy = by;
ApprovedOn = DateTime.Today;
IsApproved = true;
Raise(new ApprovedEvent(Title));
}
public void Publish(String by)
{
if (IsPublished)
throw new InvalidOperationException("Already published.");
if (!IsApproved)
throw new InvalidOperationException("Cannot publish until approved.");
PublishedBy = by;
PublishedOn = DateTime.Today;
IsPublished = true;
Raise(new PublishedEvent(Title));
}
}
Я хочу написать модульные тесты, которые проверяют:
- Я не могу опубликовать, если документ не был утвержден
- Я не могу повторно опубликовать документ
- Когда опубликовано, значения ОпубликованоBy и ОпубликованоOn установлены правильно
- Когда опубликовано, ОпубликованоEvent повышается
Без доступа к установщикам я не могу перевести объект в состояние, необходимое для выполнения тестов. Открытие доступа к сеттерам сводит на нет цель предотвращения доступа.
Как (есть) вы решили (d) эту проблему?
источник
Ответы:
Если вы не можете перевести объект в состояние, необходимое для выполнения теста, вы не можете перевести объект в состояние в производственном коде, поэтому нет необходимости проверять это состояние. Очевидно, что это не так в вашем случае, вы можете перевести ваш объект в нужное состояние, просто вызовите Approve.
Я не могу опубликовать, если Документ не был утвержден: напишите тест, в котором вызов публикации перед вызовом утверждения вызывает правильную ошибку без изменения состояния объекта.
Я не могу повторно опубликовать документ: напишите тест, который утверждает объект, затем вызовите публикацию, если один раз успешно, но второй раз вызывает правильную ошибку без изменения состояния объекта.
Когда публикуется, значения ОпубликованоBy и ОпубликованоOn установлены правильно: напишите тест, который вызывает утверждение, затем вызовите публикацию, утверждая, что состояние объекта изменяется правильно
При публикации публикуется событиеEventEvent: подключите систему событий и установите флаг, чтобы убедиться, что она вызывается.
Вам также нужно написать тест для утверждения.
Другими словами, не проверяйте отношения между внутренними полями и IsPublished и IsApproved, ваш тест будет довольно хрупким, если вы сделаете это, поскольку изменение вашего поля будет означать изменение кода вашего теста, поэтому тест будет совершенно бессмысленным. Вместо этого вы должны проверить связь между вызовами открытых методов, таким образом, даже если вы изменяете поля, вам не нужно изменять тест.
источник
setup()
метод, а не в сам тест.approve()
как-то хрупко, ноsetApproved(true)
как-то нет?approve()
является законной зависимостью в тестах, потому что это зависимость в требованиях. Если бы зависимость существовала только в тестах, это было бы другой проблемой.push()
иpop()
методы самостоятельно?Еще один подход заключается в создании конструктора класса, который позволяет устанавливать внутренние свойства при создании экземпляра:
источник
Одна из стратегий заключается в том, что вы наследуете класс (в данном случае Document) и пишете тесты для унаследованного класса. Унаследованный класс позволяет каким-то образом установить состояние объекта в тестах.
В C # одна стратегия может заключаться в том, чтобы сделать сеттеры внутренними, а затем подвергнуть внутренние компоненты тестовому проекту.
Вы также можете использовать API класса, как вы описали («Я, конечно, могу сделать это, дважды позвонив в Publish»). Это будет установка состояния объекта с использованием общедоступных сервисов объекта, мне это не кажется слишком вонючим. В случае с вашим примером, вероятно, я бы так и сделал.
источник
Чтобы протестировать в абсолютной изоляции команды и запросы, которые получают доменные объекты, я использовал для каждого теста сериализацию объекта в ожидаемом состоянии. В разделе аранжировки теста загружается объект для тестирования из файла, который я предварительно подготовил. Сначала я начал с двоичных сериализаций, но оказалось, что json гораздо проще поддерживать. Это доказало свою эффективность, когда абсолютная изоляция в тестах дает реальную ценность.
отредактируйте только примечание, иногда JSON-сериализация завершается неудачно (как в случае с циклическими графами объектов, которые являются запахом, кстати). В таких ситуациях я спасаю бинарную сериализацию. Это немного прагматично, но работает. :-)
источник
Ты говоришь
а также
и я должен думать, что использование отражения для обхода контроля доступа в ваших классах - это не то, что я бы назвал «лучшей практикой» Это будет ужасно медленно тоже.
Лично я бы отказался от вашей среды модульных тестов и занялся чем-то в классе - похоже, вы пишете тесты с точки зрения тестирования всего класса в любом случае, и это хорошо. В прошлом для некоторых хитрых компонентов, которые требовали тестирования, я встраивал код утверждений и настройки в сам класс (раньше это был общий шаблон проектирования, в котором метод test () использовался в каждом классе), поэтому вы создавали клиент это просто создает экземпляр объекта и вызывает тестовый метод, который может настроить себя, как вам нравится, без всяких мерзостей, таких как рефлексы.
Если вы беспокоитесь о раздувании кода, просто оберните методы тестирования в #ifdefs, чтобы сделать их доступными только в отладочном коде (вероятно, самой лучшей практикой)
источник