8

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

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

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

  • Автор
  • категорию
  • список Bookstores вы можете найти книгу в

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

Теперь мы хотим провести тестирование метода BookRepository, который возвращает все книги. Чтобы проверить, возвращает ли метод книги, мы должны настроить тестовый контекст (шаг Arrange в терминах 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); 
} 

Учитывая, что единственным инструментом в нашем распоряжении для создания объектов Book является завод, тест блок теперь использует и зависит от завода и inderectly по категориям, Автора и магазин, так как нам нужны эти объекты, чтобы создать книгу, а затем поместить ее в тестовый контекст.

Считаете ли вы, что это зависимость так же, как в тесте Service unit мы будем зависеть, скажем, от репозитория, который будет вызывать Служба?

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

Если вы макет вещи Repository содержит, какого рода фиктивных/окурков бы вы использовать в отличие от того, когда вы импровизировать что-то тестируемый объект переговоров с или потребляет?

ответ

4

Две вещи:

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

  • Что касается комплекса, в какой-то момент вам понадобятся некоторые действительные книги. Извлеките эту логику в метод настройки, который будет выполняться перед каждым тестом. Создайте этот метод для создания достоверной коллекции книг и т. Д.

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

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

1

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

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

Я предполагаю, что сочетание созданных с помощью изоляционных фраз и методов создания может помочь мне прояснить и упростить мои тесты.

1

Возможно, вы захотите попробовать Test Data Builder. Nice post from Nat Pryce.

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

3

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

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

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

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

1

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

частный 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)); 
    } 

} 
0

II может быть предвзятым, потому что я начал по обучению DDD с CQRS. Но я не уверен, что вы нарисуете правильные границы. Агрегат должен знать только о своих инвариантах. Вы говорите, что у книги есть автор. Да, но книга не имеет никакого инварианта по имени автора. , чтобы мы могли представить совокупную книгу следующим образом:

public class Book 
{ 
    public Guid _idAuthor; 

    public Book(Guid idAuthor) 
    { 
     if(idAuthor==guid.empty) throw new ArgumentNullException(); 

     _idAuthor = idAuthor; 
    } 
} 

Принимая во внимание, автор имеет инвариантный на его авторе:

public class Author 
{ 
    public string _name; 

    public Book(string name) 
    { 
     if(name==nullorEmpty) throw new ArgumentNullException(); 

     _name= name; 
    } 
} 

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

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

При создании совокупности Закажите свой модульный тест, чтобы он стал проще, потому что вы фокусируетесь на стороне записи и на истинных инвариантах.