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
相关产品推荐
相关产品推荐

