Как сохранить Ваши модульные тесты простыми и изолированными и все еще гарантировать инварианты DDD?

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

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

Давайте предположим, что у нас есть BookRepository, который содержит Книги. Книга имеет:

  • Автор
  • Категория
  • список Книжных магазинов можно найти книгу в

Они требуются атрибуты: книга должна иметь автора, категорию и по крайней мере книжный магазин, у которого можно купить книгу. Вероятно, будет BookFactory, так как это - настоящий сложный объект, и Фабрика инициализирует Книгу по крайней мере со всеми упомянутыми атрибутами. Возможно, мы также сделаем Книжного конструктора частным (и вложенная Фабрика) так, чтобы никто не мог инстанцировать пустой Книги кроме Фабрики.

Теперь мы хотим к модульному тесту метод BookRepository, который возвращает все Книги. Чтобы протестировать, если метод возвращает книги, мы должны настроить тестовый контекст (шаг Расположения в терминах AAA), где некоторые Книги уже находятся в Репозитории.

В C#:

[Test]
public void GetAllBooks_Returns_All_Books() 
{
    //Lengthy and messy Arrange section
    BookRepository bookRepository = new BookRepository();
    Author evans = new Author("Evans", "Eric");
    BookCategory category = new BookCategory("Software Development");
    Address address = new Address("55 Plumtree Road");
    BookStore bookStore = BookStoreFactory.Create("The Plum Bookshop", address);
    IList<BookStore> bookstores = new List<BookStore>() { bookStore };
    Book domainDrivenDesign = BookFactory.Create("Domain Driven Design", evans, category, bookstores);
    Book otherBook = BookFactory.Create("other book", evans, category, bookstores);
    bookRepository.Add(domainDrivenDesign);
    bookRepository.Add(otherBook);

    IList<Book> returnedBooks = bookRepository.GetAllBooks();

    Assert.AreEqual(2, returnedBooks.Count);
    Assert.Contains(domainDrivenDesign, returnedBooks);
    Assert.Contains(otherBook, returnedBooks);
}

Учитывая, что единственным инструментом в нашем распоряжении для создания Книжных объектов является Фабрика, модульный тест теперь использует и зависит от Фабрики и косвенно от Категории, Автора и Хранилища, так как нам нужны те объекты создать Книгу и затем поместить ее в тестовый контекст.

Вы полагали бы, что это - зависимость таким же образом, что в Сервисном модульном тесте мы зависели бы от, скажем, Репозитория, который назовет Сервис?

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

Если бы Вы копируете вещи, Репозиторий содержит, какую насмешку/тупики Вы использовали бы в противоположность тому, когда Вы копируете что-то, с чем объект под тестом говорит или использует?

9
задан guillaume31 14 May 2010 в 12:35
поделиться

5 ответов

Вы можете попробовать Построитель тестовых данных . Хороший пост от Ната Прайса .

Это может помочь, если вы не хотите идти по пути моков. Он может абстрагироваться от всех этих уродливых фабричных методов. Также вы можете попробовать подтолкнуть компоновщики для использования в вашем производственном коде.

1
ответ дан 4 December 2019 в 21:08
поделиться

Спасибо, Финглас за ответ. Я использую макеты в других тестах, но в первую очередь для тестирования взаимодействия, а не для настройки контекста теста. Я не был уверен, можно ли назвать такой полый объект только с необходимыми значениями имитацией и стоит ли их использовать.

Я нашел кое-что интересное и довольно близкое к проблеме на сайте xunitpatterns.com Джерарда Месароса. Он описывает запах кода, связанный с длинной и сложной тестовой установкой, как нерелевантная информация , с возможными решениями методы создания или фиктивные объекты . Я не совсем уверен в его реализации фиктивного объекта, поскольку в моем примере это заставило бы меня иметь интерфейс IBook (тьфу), чтобы реализовать фиктивную книгу с очень простым конструктором и обойти всю логику создания Factory.

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

1
ответ дан 4 December 2019 в 21:08
поделиться

Может быть, мы тоже сделаем Книгу конструктор частный (и Завод nested), чтобы никто не мог создать экземпляр пустая Книга, кроме Фабрики.

Частный конструктор Book является источником ваших проблем.

Если вместо этого сделать конструктор Book внутренним, фабрику не нужно вкладывать. Тогда вы можете заставить фабрику реализовать интерфейс (IBookFactory), и вы можете внедрить макет книжной фабрики в свой репозиторий.

Если вы действительно хотите убедиться, что экземпляры создаются только в реализациях книжной фабрики, добавьте в репозиторий метод, который принимает аргументы, необходимые фабрике:

public class BookRepository {

    public IBookFactory bookFactory;

    public BookRepository(IBookFactory bookFactory) {
        this.bookFactory = bookFactory;
    }

    // Abbreviated list of arguments
    public void AddNew(string title, Author author, BookStore bookStore) {
        this.Add(bookFactory.Create(title, author, bookStore));
    }

}
1
ответ дан 4 December 2019 в 21:08
поделиться

Две вещи:

  • Используйте mock-объекты в тестах. В настоящее время вы используете конкретные объекты.

  • Что касается сложной установки, то в какой-то момент вам понадобятся валидные книги. Вынесите эту логику в метод set up, который будет запускаться перед каждым тестом. Пусть этот метод создаст действительную коллекцию книг и так далее.

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

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

4
ответ дан 4 December 2019 в 21:08
поделиться

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

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

  • создать набор тестовых приборов . Это небольшие, но концептуально полные наборы данных для использования в ваших тестах. Обычно они хранятся в некоторой сериализованной форме (xml, csv, sql) и загружаются в начале каждого теста в вашу базу данных, чтобы у вас было действительное состояние. На самом деле это просто обычная фабрика, которая работает путем чтения статических файлов.

Если вы используете приборы, вы можете использовать подход с одним или несколькими приборами. Если вы можете обойтись одним «каноническим» набором данных для большинства ваших модульных тестов, это будет проще, но иногда это создает набор данных, в котором слишком много записей, чтобы их можно было понять, или просто не выражающий диапазон. сценариев, которые необходимо поддерживать. Некоторые проблемы требуют тщательного тестирования нескольких наборов данных.

3
ответ дан 4 December 2019 в 21:08
поделиться
Другие вопросы по тегам:

Похожие вопросы: