How to keep your unit tests simple and isolated and still guarantee DDD invariants?
Asked Answered
P

7

11

DDD recommends that the domain objects should be in a valid state at any time. Aggregate roots are responsible for guaranteeing the invariants and Factories for assembling objects with all the required parts so that they are initialized in a valid state.

However this seems to complicate the task of creating simple, isolated unit tests a lot.

Let's assume we have a BookRepository that contains Books. A Book has :

  • an Author
  • a Category
  • a list of Bookstores you can find the book in

These are required attributes : a book has to have an author, a category and at least a book store you can buy the book from. There's likely to be a BookFactory since it is quite a complex object, and the Factory will initialize the Book with at least all the mentioned attributes. Maybe we'll also make the Book constructor private (and the Factory nested) so that no one can instantiate an empty Book except the Factory.

Now we want to unit test a method of the BookRepository that returns all the Books. To test if the method returns the books, we have to set up a test context (the Arrange step in AAA terms) where some Books are already in the Repository.

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);
}

Given that the only tool at our disposal to create Book objects is the Factory, the unit test now uses and is dependent on the Factory and inderectly on Category, Author and Store since we need those objects to build up a Book and then place it in the test context.

Would you consider this is a dependency in the same way that in a Service unit test we would be dependent on, say, a Repository that the Service would call ?

How would you solve the problem of having to re-create a whole cluster of objects in order to be able to test a simple thing ? How would you break that dependency and get rid of all these Book attributes we don't need in our test ? By using mocks or stubs ?

If you mock up things a Repository contains, what kind of mock/stubs would you use as opposed to when you mock up something the object under test talks to or consumes ?

Pinto answered 14/5, 2010 at 10:33 Comment(0)
P
4

Two things:

  • Use mock objects within the tests. You're currently using concrete objects.

  • With regards the complex set up, at some point you will need some valid books. Extract this logic to a set up method, to run before each test. Have that set up method create a valid collection of books and so forth.

"How would you solve the problem of having to re-create a whole cluster of objects in order to be able to test a simple thing ? How would you break that dependency and get rid of all these Book attributes we don't need in our test ? By using mocks or stubs ?"

A mock object would let you do this. If a test only needs a book with a valid author, your mock object would specify that author, the other attributes would be defaulted. As your test only cares about a valid author, there is no need to set up the other attributes.

Pedaias answered 14/5, 2010 at 12:45 Comment(0)
F
3

For pure unit tests, mocks and stubs are definitely the solution. But since you are going after a more integration level tests, and mocks (or stubs or whatever) aren't solving your problem, you really have two reasonable choices:

  • create test factories to help you set up the data you need. These will probably be test-specific, which not only build up a bookstore, but populate it with a reasonable set up books. That way you compress your setup code down into a line or two, and use them for other tests. This code may grow to create various scenarios that are needed for integration type tests.

  • create a set up test fixtures. These are small, but conceptually complete sets of data for your tests to use. These are generally stored in some sort of serialized form (xml, csv, sql), and loaded at the beginning of each tests into your database so that you have a valid state. They are really just a general factory that works by reading static files.

If you use fixtures, you can take the single or multiple fixture approach. If you can get away with a single "canonical" set of data for most of your unit tests, that will be simpler, but sometimes that creates a data set that has too many records to be understandable, or simply doesn't express the range of scenarios you need to support. Some problems require multiple sets of data to be thoroughly tested.

Fulgurous answered 17/5, 2010 at 15:0 Comment(0)
P
2

Thanks Finglas for the answer. I do use mocks in other tests but primarily for interaction testing, not for setting up the test context. I was not sure whether this kind of hollow object with just the needed values could be called a mock and if it was a good idea to use them.

I found something interesting and pretty close to the problem at Gerard Meszaros' xunitpatterns.com. He describes the code smell of having a long and complicated test setup as Irrelevant Information, with possible solutions being Creation Methods or Dummy Objects. I'm not entirely sold on his Dummy Object implementation though, since in my example it would force me to have an IBook interface (ugh) in order to implement a dummy Book with a very simple constructor and bypass all the Factory creation logic.

I guess a mix of isolation-framework-generated mocks and creation methods could help me clarify and simplify my tests.

Pinto answered 14/5, 2010 at 15:13 Comment(0)
K
1

You might want to try a Test Data Builder. Nice post from Nat Pryce.

This can help if you don't want to go the route of mocks. It can abstract away all those ugly factory methods. Also you can try to push the builders to be used in your production code.

Kofu answered 14/5, 2010 at 16:38 Comment(0)
C
1

Maybe we'll also make the Book constructor private (and the Factory nested) so that no one can instantiate an empty Book except the Factory.

The private Book constructor is source of your problems.

If you make Book's constructor internal instead, the factory doesn't have to be nested. Then you're free to make the factory implement an interface (IBookFactory), and you can inject a mock book factory into your repository.

If you really want to ensure that only book factory implementations create instances, add a method to your repository that accepts the arguments the factory needs:

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));
    }

}
Concert answered 17/5, 2010 at 15:5 Comment(0)
R
0

II might be biased because I have started learning DDD along side with CQRS. But I am not sure you draw the correct boundaries. An aggregate should know only about its invariants. You say a book has an author. Yes but the book has no invariant on the name of the author. so we could picture the aggregate book as follows :

 public class Book
 {
     public Guid _idAuthor;

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

         _idAuthor = idAuthor;
     }
 }

Whereas, the Author has an invariant on its author :

 public class Author
 {
     public string _name;

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

         _name= name;
     }
 }

The query side though would might have a need for both the information book name and author name, but this is a query and might not be suited for unit testing IMO.

If you need to be able to add to your library, only book when their author hhas the letter 'e' in it, then the whole discussion is different, but from what I understood, you do not need it right now.

When creating the aggregate Book your unit test becomes simpler then, because you focus on the write side and on the true invariants.

Roughrider answered 2/1, 2013 at 15:6 Comment(0)
T
0

If I understand the question correctly, the OP wants to reduce the clutter in setting up each and somehow create the hierarchy of domain objects easily. If this is the case, then [https://github.com/AutoFixture/AutoFixture] is a great tool. Or if the question is about why we should create all the objects to create another domain object, I guess the answer is "It depends". If the system under test (SUT) is an aggregate root then it means it deals with life cycle of all other objects anyways, if SUT is some other object, then AutoFixture can help creating those objects for us. It is completely customizable

Talbert answered 3/9, 2019 at 22:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.