单元测试领域对象

3
我们有一个要求,在用户在事件页面输入他们的电子邮件地址时添加一个事件提醒。事件是另一个域对象。我们最初的想法是创建一个客户端域对象和相关的客户端服务:
public class CustomerService {
    public void AddEventReminder(string emailAddress, int eventId) {
       var customer = new Customer(emailAddress);
       customer.AddEmailReminder(eventId);
    }
}

我们如何在单元测试中验证AddEmailReminder方法确实被调用了?
我的想法:
1.使用工厂创建客户。这样做有点不妥,因为我认为只有在对象创建中存在某些复杂性时才应该使用工厂。
2.糟糕的代码。也许有更好的方法吗?
3.Moq魔法。
另外(也许与此相关),我们如何决定哪个是聚合根?我们已经任意地决定了客户端,但同样可以是事件。我已经阅读并理解了关于聚合根的文章,但在这种情况下仍然不清楚。
2个回答

6
在这种情况下,我会在服务中创建一个受保护的方法来创建客户,在测试中使用匿名内部类覆盖该方法,并使其返回模拟的客户对象。然后,您可以验证模拟的客户对象上是否调用了AddEmailReminder。 类似这样:
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);
    }
}

在测试中(假设有限的C#知识,但这应该能说明问题):

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

2

对于CustomerService API的思考

你是否有特别的原因决定将这个操作封装在CustomerService中?这看起来有点贫血。它可能直接封装在Customer上吗?

也许你在CustomerService代码示例中简化了一些内容...

然而,如果必须这样做,将签名更改为使用Customer实例可以解决问题:

public void AddEventReminder(Customer customer, int eventId)

但是,Int32很难被视为领域对象,因此签名应该是:

public void AddEventReminder(Customer customer, Event event)

现在的问题是这种方法是否增加了任何价值?

哪一个是聚合根?

我认为它们都不是。聚合根表示你只通过根来管理子项,但在这种情况下无论哪种方式都不合理。

考虑以下选项:

如果你将Event作为根,这意味着你不能有CustomerRepository,而且你检索、编辑和保存客户的唯一途径是通过事件。这对我来说听起来非常不正确。

如果你将Customer作为根,你就不能使用EventRepository,而且你检索、编辑和保存事件的唯一途径是通过特定的客户。这对我来说同样不正确。

唯一剩下的可能性是它们是分开的根。这也意味着它们之间只有松散的连接,并且你需要某种领域服务来查找客户的事件或事件的客户。


这个域服务方法的整个重点在于它直接从应用程序服务中调用,该服务已收到带有电子邮件地址和事件ID的DTO。我认为域对象只应处理完整对象,因此域服务应处理此问题。我不明白为什么它是无血统的意味着它不应该进入域服务,如果有的话,我认为情况会相反,因为域对象都涉及逻辑。关于聚合根,我同意它们都是聚合根,我问的问题是哪个聚合根负责关联? - JontyMC
虽然这是一个有趣的答案,但如果我要更改签名,哪些代码将负责创建客户聚合根呢?也许这才是更重要的问题。 - JontyMC
我怀疑这个服务是从应用层进行映射的。然而,在这种情况下,它不是一个领域服务 - 它是一个应用层服务,并且属于该层。在这种情况下,我认为您需要一个抽象工厂(也许是一个CustomerRepository),它可以根据电子邮件地址查找现有的客户。 - Mark Seemann
关于与事件和客户相关联的责任问题:当它们互相都不是聚合根时,它们都无法承担这个责任。一个新的域服务必须承担这一责任。 - Mark Seemann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接