【问题标题】:How to unit test properly aggregate roots?如何正确进行单元测试聚合根?
【发布时间】:2022-01-07 17:04:04
【问题描述】:

让我们考虑下面的例子:

    internal class Meeting
    {
        public int Id { get; set; }
    }

    internal class DailyRoomReservation
    {
        private ISet<Meeting> _meetings { get; set; } = new HashSet<Meeting>();

        internal void ScheduleMeeting(Meeting meeting)
        {
            if (_meetings.Contains(meeting)) throw new InvalidOperationException();
            _meetings.Add(meeting);
        }
    }

假设DailyRoomReservation 是我的聚合根(为了简单起见,我故意省略了大部分业务逻辑),我应该如何测试它?仅公开聚合的命令方法(CQS 术语)是已知的良好做法,尤其是在大图使用 CQRS 时。此外,我没有公开_meetings 属性的业务需要(测试目的当然不是这样做的好理由)。我写了以下测试:

    [Test]
    internal void ScheduleNewMeeting_ShouldSucceed()
    {
        var uniqueMeeting = new Meeting() { Id = 1};
        var dailyRoomReservation = new DailyRoomReservation();
        dailyRoomReservation.ScheduleMeeting(uniqueMeeting);
    }
    
    [Test]
    internal void ScheduleSameMeetingTwice_ShouldFail()
    {
        var meeting = new Meeting() { Id = 1};
        var dailyRoomReservation = new DailyRoomReservation();
        dailyRoomReservation.ScheduleMeeting(meeting);

        Action scheduleMeeting = () => dailyRoomReservation.ScheduleMeeting(meeting);
        scheduleMeeting.Should().ThrowExactly<InvalidOperationException>();
    }

而且它们工作得很好,但是我仍然无法验证是否真的添加了会议。如何改进我的方法?

【问题讨论】:

  • 即使没有暴露,_meeting 也必须有一些效果,而不仅仅是存在。它是干什么用的?这能提供一种测试方法吗?还是可以将其公开为只读集合?
  • 是的,我的系统多次读取它,但所有这些操作的入口点都是查询(正如我所提到的,我试图在架构级别应用 CQRS)。可以通过发送添加会议的命令进行测试,然后通过查询检索保存的数据。但它只能在低级别的黑盒方法中实现(因此没有经过单元测试)。
  • 请问您是如何将聚合状态暴露给持久层的?
  • 曝光是什么意思?我如何允许 ORM 读取它的状态?如果是 - 我使用实体框架和字符串导航属性:builder.Entity&lt;DailyRoomReservation&gt;().HasMany("_meetings")

标签: c# .net unit-testing domain-driven-design aggregateroot


【解决方案1】:

启发式:只写域实体实际上并不提供任何商业价值。

如果您将信息放入域实体中,您这样做的目的是期望从中产生的信息可能会发生一些变化。

因此,我们测试的基本结构是我们获得一个实体的实例,我们向它发送一系列命令,然后我们测量出来的信息。如果测量符合某个预定规范(换句话说,如果断言通过),我们的域实体通过测试。

我们可以通过至少四种不同的方式来获取信息。

首先,我们实际上可以进入并查看内部数据模型。在某些情况下,这很好(通常是一次性测试,也称为脚手架,我们不希望将其兼作文档)。

其次,我们可以查询实体以获取信息 - 当您有一个预期支持该查询作为其域表示的一部分的实体时非常适合。

第三,我们可以窃听该实体发送到我们代码其他部分的消息,并以这种方式捕获信息的副本。这是tell-dont-ask设计中的一种常见方法,我们使用测试替身/替代来捕获我们想要评估的信息。

第四,如果我们期望持久存储域数据,我们可以“存储”实体,然后 (a) 检查其持久表示或 (b) 将该表示加载到“读取模型”并查询.

在将测试和可观察性视为首要问题的上下文中,我们当然会实现一个查询方法以允许访问我们想要验证的信息的副本。我们实体的实现包含这样一个方法这一事实并不一定意味着该方法是已发布接口的一部分。


在这个特定的示例中,您可以使用“它会抛出吗?”作为您的度量。我希望看到至少四次测试

  • 添加一个会议不会引发
  • 添加两个具有不同标识符的会议不会引发
  • 两次添加同一个会议确实会引发
  • 添加两个具有相同标识符的会议确实会引发

但是在我们希望将重复会议的安排视为无操作的领域中,这种测量并不令人满意,我们需要编写测试来检测其他类型的差异。

【讨论】:

  • 非常感谢您的回答,但为了更好地理解,我想向您展示我想到的针对您提出的每种类型的示例。 首先 - 您的意思是通过使用反射来研究内部数据模型还是仅仅为了测试目的而公开其属性? 第二-然后在低级别完成(例如模块测试?)第三-假设每个聚合根都具有DomainEvents属性,我可以测试是否它包含例如MeetingScheduledEvent? 第四种 - 我看不出这一种和第二种类型之间的区别?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-23
  • 2014-01-30
  • 2011-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多