You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CQRS架构下CommandHandler的测试方法、范围及实践示例问询

CommandHandler测试指南:方法、范围与实践示例

核心问题解答

1. 这类处理器到底是否需要测试?

必须测试。CommandHandler是业务逻辑的核心入口,负责协调实体状态变更、数据持久化、领域事件发布三大关键流程,一旦出错会直接破坏数据一致性、中断业务流转,是测试的核心对象。

2. 测试范围应包含哪些内容?

  • 业务逻辑正确性:命令输入是否被正确转化为实体状态变更(比如新增分类的名称是否正确、重命名后的实体名称是否更新)
  • 依赖交互正确性:是否按预期调用了Repository的save/getById方法,是否正确发布了实体产生的领域事件
  • 输出/执行结果正确性:新增场景返回的ID是否符合格式,修改场景是否无异常完成执行

3. 单元测试还是集成测试?两者都需要,侧重点不同

  • 单元测试:优先做,隔离外部依赖,专注验证Handler自身的逻辑正确性,执行速度快、反馈及时,适合日常开发中的快速验证
  • 集成测试:补充做,验证Handler与真实依赖(比如数据库、消息队列)的协作是否正常,确保端到端流程的正确性,适合回归测试阶段

4. 是否需要使用Mock?视依赖场景而定

  • 用Mock的场景:当依赖是复杂外部服务(比如真实数据库、第三方消息队列),或者需要验证Handler对依赖的调用逻辑(比如是否调用了save方法、调用次数是否正确)时,用Mock隔离依赖,避免外部因素干扰测试
  • 不用Mock的场景:当依赖有简单的内存测试实现(比如TestCategoryRepository),直接用真实测试实现更直观,能真实反映依赖的行为,减少Mock的维护成本

实用测试示例与流程

以下示例基于PHP+PHPUnit工具,结合你的现有代码优化:

先明确处理器代码(补充业务逻辑注释)

// 新增分类命令处理器
public function __invoke(AddCategory $command): int
{
    // 业务逻辑:根据命令参数创建Category实体
    $category = Category::create($command->name(), $command->parentId());

    // 持久化实体到仓库
    $this->categories->save($category);
    // 发布实体产生的领域事件(比如CategoryCreated)
    $this->events->publishAll($category->releaseEvents());

    // 返回新增分类的ID
    return $category->id()->value;
}

// 重命名分类命令处理器
public function __invoke(RenameCategory $command): void
{
    // 业务逻辑:从仓库获取目标分类实体
    $category = $this->categories->getById($command->categoryId());
    // 调用实体方法修改名称(领域逻辑封装在实体内部)
    $category->rename($command->newName());

    // 持久化更新后的实体
    $this->categories->save($category);
    // 发布实体产生的领域事件(比如CategoryRenamed)
    $this->events->publishAll($category->releaseEvents());
}

示例1:新增分类的单元测试(用测试实现依赖)

use PHPUnit\Framework\TestCase;

class AddCategoryHandlerTest extends TestCase
{
    private TestCategoryRepository $categories;
    private TestEventDispatcher $events;
    private AddCategoryHandler $handler;

    protected function setUp(): void
    {
        // 使用内存实现的测试依赖,无需Mock,贴近真实行为且速度快
        $this->categories = new TestCategoryRepository();
        $this->events = new TestEventDispatcher();
        $this->handler = new AddCategoryHandler($this->categories, $this->events);
    }

    public function test_新增分类后_实体持久化且事件发布成功(): void
    {
        // 准备测试数据
        $command = new AddCategory("电子产品", null);
        $initialEventCount = $this->events->getEventCount();
        $initialRepoCount = $this->categories->getCount();

        // 执行命令
        $categoryId = ($this->handler)($command);

        // 验证结果
        $this->assertIsInt($categoryId); // 返回ID格式正确
        $this->assertEquals($initialRepoCount + 1, $this->categories->getCount()); // 仓库新增了实体
        $this->assertEquals($initialEventCount + 1, $this->events->getEventCount()); // 事件成功发布
        $savedCategory = $this->categories->getById($categoryId);
        $this->assertNotNull($savedCategory);
        $this->assertEquals("电子产品", $savedCategory->name()->value); // 实体属性符合预期
    }
}

测试原因:验证Handler从命令输入到实体创建、持久化、事件发布的完整逻辑,用内存测试依赖避免外部干扰,确保业务逻辑的正确性。


示例2:重命名分类的单元测试(用Mock隔离依赖)

class RenameCategoryHandlerTest extends TestCase
{
    private CategoryRepositoryInterface $categories;
    private TestEventDispatcher $events;
    private RenameCategoryHandler $handler;

    protected function setUp(): void
    {
        // Mock仓库依赖,专注验证Handler对依赖的调用逻辑
        $this->categories = $this->createMock(CategoryRepositoryInterface::class);
        $this->events = new TestEventDispatcher();
        $this->handler = new RenameCategoryHandler($this->categories, $this->events);
    }

    public function test_重命名分类时_正确调用依赖并发布事件(): void
    {
        // 准备测试数据
        $categoryId = 1;
        $newName = "新分类名";
        // Mock Category实体,验证业务方法调用
        $category = $this->createMock(Category::class);
        $category->expects($this->once())
                 ->method('rename')
                 ->with(new CategoryName($newName));

        // 验证仓库方法的调用逻辑
        $this->categories->expects($this->once())
                         ->method('getById')
                         ->with($categoryId)
                         ->willReturn($category);
        $this->categories->expects($this->once())
                         ->method('save')
                         ->with($category);

        $command = new RenameCategory($categoryId, $newName);
        $initialEventCount = $this->events->getEventCount();

        // 执行命令
        ($this->handler)($command);

        // 验证事件发布
        $this->assertEquals($initialEventCount + 1, $this->events->getEventCount());
    }
}

测试原因:重命名逻辑依赖仓库的getById和save方法,用Mock可以隔离仓库的内部实现,专注验证Handler是否正确协调了依赖调用,确保流程符合预期。


示例3:新增分类的集成测试(用真实依赖)

class AddCategoryHandlerIntegrationTest extends TestCase
{
    private PdoCategoryRepository $categories;
    private RabbitMqEventDispatcher $events;
    private AddCategoryHandler $handler;

    protected function setUp(): void
    {
        // 使用真实的数据库仓库和消息队列事件分发器
        $pdo = new PDO('mysql:host=localhost;dbname=test_db', 'test_user', 'test_pass');
        $this->categories = new PdoCategoryRepository($pdo);
        $this->events = new RabbitMqEventDispatcher('amqp://guest:guest@localhost:5672/');
        $this->handler = new AddCategoryHandler($this->categories, $this->events);

        // 测试前清空测试数据,避免脏数据影响
        $pdo->exec('TRUNCATE TABLE categories');
    }

    public function test_新增分类后_数据持久化到数据库且事件发送到消息队列(): void
    {
        $command = new AddCategory("家电", null);

        $categoryId = ($this->handler)($command);

        // 验证数据库中存在该分类
        $savedCategory = $this->categories->getById($categoryId);
        $this->assertNotNull($savedCategory);
        $this->assertEquals("家电", $savedCategory->name()->value);

        // 验证消息队列收到事件(根据事件分发器的实现编写消费逻辑)
        $event = $this->events->consumeNextEvent();
        $this->assertInstanceOf(CategoryCreated::class, $event);
        $this->assertEquals($categoryId, $event->categoryId()->value);
    }
}

测试原因:验证Handler与真实外部依赖的协作是否正常,确保数据真正写入数据库、事件真正发送到消息队列,避免单元测试中无法发现的依赖适配问题。


内容的提问来源于stack exchange,提问作者Alexey Shimansky

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 21:19:51