Modular Monolith DDD单元测试模式:Given-When-Then详解

Modular Monolith DDD单元测试模式:Given-When-Then详解

【免费下载链接】modular-monolith-with-ddd Full Modular Monolith application with Domain-Driven Design approach. 【免费下载链接】modular-monolith-with-ddd 项目地址: https://gitcode.com/GitHub_Trending/mo/modular-monolith-with-ddd

引言:你还在为DDD领域层测试头疼吗?

在模块化单体(Modular Monolith)架构中,领域驱动设计(DDD)的单元测试面临双重挑战:既要验证复杂业务规则,又要确保测试代码与领域模型同步演进。传统的Arrange-Act-Assert(AAA)模式在表达领域行为时往往显得冗长,而Given-When-Then(GWT)模式通过场景化语言领域事件验证,为DDD单元测试提供了更精准的表达范式。本文将深入剖析GWT模式在Modular Monolith架构中的实现方法,通过3个核心模块的12个测试案例,教你如何构建可维护、高可读性的领域层测试。

读完本文你将掌握:

  • GWT模式与DDD战术设计的映射关系
  • 领域事件驱动的断言方法实现
  • 跨模块测试代码复用技巧
  • 复杂业务规则的场景化测试策略

Given-When-Then模式与DDD的契合点

从AAA到GWT的范式转换

传统单元测试多采用Arrange-Act-Assert(AAA)模式,而在DDD领域层测试中,GWT模式能更好地表达领域行为场景。两者的核心区别在于:

维度Arrange-Act-AssertGiven-When-Then
核心目标验证方法返回值验证领域状态变迁与事件发布
领域相关性技术实现导向业务场景导向
可读性依赖开发人员注释自然语言描述业务规则
事件验证需手动跟踪状态内置领域事件断言机制

GWT模式的DDD适配公式

Given(领域初始状态) 
  .And(前置条件满足)
When(领域行为触发)
Then(领域事件发布)
  .And(业务规则验证)

领域事件驱动的断言设计

在Modular Monolith架构中,领域事件(Domain Event)是模块间通信的核心机制。GWT模式通过验证事件发布来确保领域行为正确性,而非直接检查实体属性。这种设计与项目中的TestBase类实现高度契合:

// 源自BuildingBlocks测试基础设施
public static T AssertPublishedDomainEvent<T>(AggregateRoot aggregate)
    where T : IDomainEvent
{
    var domainEvent = aggregate.GetDomainEvents().OfType<T>().SingleOrDefault();
    if (domainEvent == null)
    {
        throw new Exception($"{typeof(T).Name} event not published");
    }
    return domainEvent;
}

核心模块测试案例深度解析

1. 会议模块(Meetings):聚合根状态变迁测试

测试场景:取消未开始的会议

[Test]
public void CancelMeeting_WhenMeetingHasNotStarted_IsSuccessful()
{
    // Given - 构建初始领域状态
    var creatorId = new MemberId(Guid.NewGuid());
    var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions
    {
        CreatorId = creatorId,
        // 会议时间设置为未来(未开始状态)
        MeetingTerm = MeetingTerm.CreateNewBetweenDates(
            DateTime.UtcNow.AddDays(2), 
            DateTime.UtcNow.AddDays(3)
        )
    });
    var cancellationDate = DateTime.UtcNow;
    SystemClock.Set(cancellationDate);

    // When - 执行领域行为
    meetingTestData.Meeting.Cancel(creatorId);

    // Then - 验证领域事件与业务规则
    var meetingCanceledEvent = AssertPublishedDomainEvent<MeetingCanceledDomainEvent>(
        meetingTestData.Meeting
    );
    meetingCanceledEvent.MeetingId.Should().Be(meetingTestData.Meeting.Id);
    meetingCanceledEvent.CancelDate.Should().Be(cancellationDate);
}

GWT模式映射分析

  • Given:通过CreateMeetingTestData方法构建会议聚合的初始状态,包含创建者ID和未来的会议时间
  • When:调用聚合根的Cancel方法触发领域行为
  • Then:验证MeetingCanceledDomainEvent事件发布,并检查事件属性与业务规则一致性

2. 支付模块(Payments):值对象驱动的测试

测试场景:订阅续订流程

[Test]
public void RenewSubscription_IsSuccessful()
{
    // Given - 构建订阅初始状态
    var subscriptionPaymentSnapshot = new SubscriptionPaymentSnapshot(
        new SubscriptionPaymentId(Guid.NewGuid()),
        new PayerId(Guid.NewGuid()),
        SubscriptionPeriod.Month,
        "PL"
    );
    var subscription = Subscription.Create(subscriptionPaymentSnapshot);

    // 构建续订支付快照(值对象)
    var renewalPayment = new SubscriptionRenewalPaymentSnapshot(
        new SubscriptionRenewalPaymentId(Guid.NewGuid()),
        new PayerId(Guid.NewGuid()),
        SubscriptionPeriod.Month,
        "PL"
    );

    // When - 执行续订操作
    subscription.Renew(renewalPayment);

    // Then - 验证订阅续订事件
    var renewalEvent = AssertPublishedDomainEvent<SubscriptionRenewedDomainEvent>(subscription);
    renewalEvent.SubscriptionId.Should().Be(subscription.Id);
    renewalEvent.Period.Should().Be(SubscriptionPeriod.Month);
}

DDD测试特性

  • 使用值对象(SubscriptionPaymentSnapshot)传递不可变状态
  • 通过聚合根工厂方法(Subscription.Create)确保创建规则被遵守
  • 事件断言验证业务时间和周期信息,而非直接访问聚合内部状态

3. 测试基础设施复用:跨模块的GWT实现

项目中的TestBase类提供了统一的GWT断言能力,支持跨模块测试代码复用:

// 业务规则违反断言
public static void AssertBrokenRule<TRule>(TestDelegate testDelegate)
    where TRule : class, IBusinessRule
{
    var exception = Assert.Catch<BusinessRuleValidationException>(testDelegate);
    Assert.That(exception.BrokenRule, Is.TypeOf<TRule>());
}

// 示例用法(会议模块)
[Test]
public void RemoveAttendee_WhenMeetingHasStarted_IsNotPossible()
{
    // Given
    var meetingTestData = CreateMeetingTestData(new MeetingTestDataOptions
    {
        // 设置已开始的会议时间
        MeetingTerm = MeetingTerm.CreateNewBetweenDates(
            DateTime.UtcNow.AddDays(-2), 
            DateTime.UtcNow.AddDays(-1)
        )
    });

    // When & Then - 验证业务规则
    AssertBrokenRule<MeetingCannotBeChangedAfterStartRule>(() =>
    {
        meetingTestData.Meeting.RemoveAttendee(new MemberId(Guid.NewGuid()), 
            meetingTestData.CreatorId, null);
    });
}

高级测试模式与最佳实践

时间相关测试的确定性处理

在DDD测试中,时间是关键的业务变量。项目通过SystemClock静态类实现时间抽象,确保测试可重复:

[Test]
public void ExpireSubscription_IsSuccessful()
{
    // Given
    var referenceDate = DateTime.UtcNow;
    SystemClock.Set(referenceDate); // 固定初始时间
    
    var subscription = Subscription.Create(CreatePaymentSnapshot());
    
    // 快进时间至订阅到期后
    SystemClock.Set(referenceDate.AddMonths(1).AddMilliseconds(1));

    // When
    subscription.Expire();

    // Then
    AssertPublishedDomainEvent<SubscriptionExpiredDomainEvent>(subscription);
}

复杂场景的GWT扩展模式

对于多步骤业务流程,可通过链式GWT表达场景:

// 伪代码表示复杂场景测试
[Test]
public void ProposeAndApproveMeetingGroup()
{
    // Given
    var proposal = MeetingGroupProposal.Create(/* 初始数据 */);
    
    // When Step 1
    proposal.Submit();
    // Then Step 1
    AssertPublishedDomainEvent<MeetingGroupProposalSubmittedEvent>(proposal);
    
    // When Step 2
    proposal.Approve();
    // Then Step 2
    var approvedEvent = AssertPublishedDomainEvent<MeetingGroupProposalApprovedEvent>(proposal);
    approvedEvent.MeetingGroupId.Should().NotBeNull();
}

测试代码组织与模块边界

模块化测试目录结构

项目遵循测试代码与生产代码结构一致原则,每个模块的测试目录镜像领域层结构:

Modules/
├── Meetings/
│   ├── Domain/
│   │   └── Meetings/
│   └── Tests/
│       └── UnitTests/
│           └── Meetings/
│               └── MeetingTests.cs
└── Payments/
    ├── Domain/
    │   └── Subscriptions/
    └── Tests/
        └── UnitTests/
            └── Subscriptions/
                └── SubscriptionTests.cs

这种结构确保测试代码与领域模型的紧密同步,同时维护模块边界。

跨模块测试依赖管理

在Modular Monolith架构中,测试代码同样遵循模块依赖规则。例如,Payments模块测试仅依赖本模块领域层和BuildingBlocks,不直接引用其他业务模块:

// Payments模块测试.csproj依赖示例
<ProjectReference Include="..\..\Domain\CompanyName.MyMeetings.Modules.Payments.Domain.csproj" />
<ProjectReference Include="..\..\..\..\BuildingBlocks\Domain\CompanyName.MyMeetings.BuildingBlocks.Domain.csproj" />

总结与进阶路线

Given-When-Then模式通过场景化描述事件驱动验证,为Modular Monolith架构下的DDD单元测试提供了清晰的方法论。本文介绍的核心实践包括:

  1. 测试范式转换:从验证状态到验证行为(事件发布)
  2. 基础设施复用:通过TestBase实现跨模块断言能力
  3. 领域规则验证AssertBrokenRule确保业务 invariants
  4. 时间抽象SystemClock实现时间相关测试的确定性

进阶学习路线

  • 探索Outbox/Inbox模式的集成测试实现
  • 研究突变测试(Mutation Testing)在领域层的应用
  • 分析CI/CD流水线中的测试自动化策略(项目docs/ci.jpg)

通过本文介绍的模式和实践,你可以构建出既符合DDD思想又易于维护的单元测试套件,为Modular Monolith架构的演进提供可靠保障。

收藏本文,并关注后续《Modular Monolith集成测试策略》系列文章!

【免费下载链接】modular-monolith-with-ddd Full Modular Monolith application with Domain-Driven Design approach. 【免费下载链接】modular-monolith-with-ddd 项目地址: https://gitcode.com/GitHub_Trending/mo/modular-monolith-with-ddd

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值