Как сохранить ваши модульные тесты простыми и изолированными и при этом гарантировать DDD-инварианты? - PullRequest
9 голосов
/ 14 мая 2010

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

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

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

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

Это обязательные атрибуты: книга должна иметь автора, категорию и, по крайней мере, книжный магазин, в котором вы можете купить книгу. Вероятно, существует BookFactory, так как это довольно сложный объект, и Factory инициализирует книгу, по крайней мере, со всеми упомянутыми атрибутами. Возможно, мы также сделаем конструктор Book частным (и вложенным в 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);
}

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

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

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

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

Ответы [ 6 ]

4 голосов
/ 14 мая 2010

Две вещи:

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

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

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

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

3 голосов
/ 17 мая 2010

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

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

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

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

1 голос
/ 17 мая 2010

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

Частный 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 голос
/ 14 мая 2010

Возможно, вы захотите попробовать Test Data Builder .Хороший пост от Nat Pryce .

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

1 голос
/ 14 мая 2010

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

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

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

0 голосов
/ 02 января 2013

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.

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

При создании агрегатной Книги ваш модульный тест становится проще, потому что вы сосредоточены на стороне записи и на истинных инвариантах.

...