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

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

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

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

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

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

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

In 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 является Factory, модульный тест теперь использует и зависит от Factory и косвенно от Category, Author и Store, поскольку нам нужны эти объекты для создания Book и последующего размещения его в тестовый контекст.

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

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

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


person guillaume31    schedule 14.05.2010    source источник


Ответы (7)


Две вещи:

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

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

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

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

person Finglas    schedule 14.05.2010

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

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

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

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

person ndp    schedule 17.05.2010

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

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

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

person guillaume31    schedule 14.05.2010

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

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

person Gutzofter    schedule 14.05.2010

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

Частный конструктор 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));
    }

}
person Jeff Sternal    schedule 17.05.2010

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

 public class Book
 {
     public Guid _idAuthor;

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

         _idAuthor = idAuthor;
     }
 }

Принимая во внимание, что Author имеет инвариант своего автора:

 public class Author
 {
     public string _name;

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

         _name= name;
     }
 }

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

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

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

person Arthis    schedule 02.01.2013

Если я правильно понимаю вопрос, ОП хочет уменьшить беспорядок при настройке каждого и каким-то образом легко создать иерархию объектов домена. Если это так, то [https://github.com/AutoFixture/AutoFixture] отличный инструмент. Или, если вопрос о том, почему мы должны создавать все объекты для создания другого объекта предметной области, я думаю, ответ будет "это зависит". Если тестируемая система (SUT) является совокупным корнем, то это означает, что она в любом случае имеет дело с жизненным циклом всех других объектов, если SUT является каким-то другим объектом, то AutoFixture может помочь нам создать эти объекты для нас. Это полностью настраиваемый

person Srivathsa Harish Venkataramana    schedule 03.09.2019