views:

93

answers:

2

We have a requirement to add an event reminder when a user enters their email address on an event page. Event is another domain object. Our initial thought was to create a Customer domain object and related CustomerService:

public class CustomerService {
    public void AddEventReminder(string emailAddress, int eventId) {
       var customer = new Customer(emailAddress);
       customer.AddEmailReminder(eventId);
    }
}

How can we verify in a unit test that the AddEmailReminder method was indeed called on the new customer?

My thoughts:

  1. Use a factory to create the customer. This smells because I thought you were only supposed to use factory where there was some complexity in the object creation.
  2. Bad code. Maybe there is a better way to do this?
  3. Moq magic.

On a separate note (maybe it is related), how do we decide which is the aggregate root here? We have arbitrarily decided the customer is, but it could equally be the event. I have read and understand articles on aggregate roots, but it is unclear in this scenario.

+4  A: 

In cases like this I would create a protected method in the service that creates the customer, in the test override that method it with anonymous inner class, and make it return a mock Customer object. Then you can verify on the mock Customer object that AddEmailReminder was called. Something like:

public class CustomerService {
    public void AddEventReminder(string emailAddress, int eventId) {
       var customer = createCustomer(emailAddress);
       customer.AddEmailReminder(eventId);
    }

    protected Customer createCustomer(string emailAddress) {
       return new Customer(emailAddress);
    }
}

and in the test (assume limited C# knowledge, but it should illustrate the point):

void testCustomerCreation() {
    /* final? */ Customer mockCustomer = new Customer("email");
    CustomerService customerService = new CustomerService() {
       protected Customer createCustomer(string emailAddress) {
           return mockCustomer;
       }            
    };

    customerService.AddEventReminder("email", 14);

    assertEquals(mockCustomer.EventReminder() /* ? */, 14);
}
Zoran Regvart
+1  A: 

Thoughts on the CustomerService API

Are there any particular reasons why you have decided to encapsulate this operation in a CustomerService? This looks a little Anemic to me. Could it possibly be encapsulated directly on the Customer instead?

Perhaps you left out something of the CustomerService code example to simplify things...

However, if you must, changing the signature to take a Customer instance solves the problem:

public void AddEventReminder(Customer customer, int eventId)

but then again, an Int32 hardly qualifies as Domain Object, so the signature should really be

public void AddEventReminder(Customer customer, Event event)

The question is now whether this method adds any value at all?

Which is the aggregate root?

None of them, I would think. An aggregate root indicates that you manage children only through the root, and that wouldn't make sense either way in this case.

Consider the options:

If you make Event the root, it would mean that you could have no CustomerRepository, and the only way you could retrieve, edit and persist a Customer would be through an Event. That sounds very wrong to me.

If you make Customer the root, you can have no EventRepository, and the only way you could retrieve, edit and persist an Event would be through a specific Customer. That sounds just as wrong to me.

The only remaining possibility is that they are separate roots. This also means that they are only loosely connected to each other, and that you will need some kind of Domain Service to look up Events for a Customer, or Customers for an Event.

Mark Seemann
The whole point of this domain service method is that it is called directly from the application service which has received a dto with the email address and event id. I figured the domain object should only deal with full objects, so the domain service should handle this. I don't see why it being anemic means it shouldn't go on the domain service, if anything I would think it would be the other way round as domain objects are all about logic. Re the aggregate root, I agree they are both aggregate roots, the question I was asking was which aggregate root is responsible for the association?
JontyMC
...interesting answer though. If I were to change the signature, what code would be responsible for creating the Customer aggregate root? Perhaps this is the more pertinent question.
JontyMC
I suspected that the service was to map from an application layer. However, in that case, it isn't a Domain Service - it's an Application Layer service and belongs in that layer. In that case I think you need an Abstract Factory (perhaps a CustomerRepository) that can look up an existing Customer based on the email address.
Mark Seemann
Regarding the question about responsibility of associating Events and Customers: when none of them are aggregate roots for each other, none of them can take on that responsibility. A new Domain Service must take on that responsibility.
Mark Seemann