8.9 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | |||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 异步事件单元测试 | 结合 slot_console 现有 Free Credits 测试实践,说明异步 MQ 事件应分层测试:不直连 RabbitMQ,优先测 Logic 编排,再补 Event 解析与 EventBus 路由;并给出可新增的示例用例结构。 |
|
false |
异步事件单元测试写法(slot_console)
当前架构
sequenceDiagram
participant Wallet as slot_wallet
participant MQ as RabbitMQ_console_bus
participant Bus as EventBus_deal
participant Ev as FreeCreditInitEvent
participant Logic as FreeCreditsLogic
Wallet->>MQ: JSON uid/type/data
MQ->>Bus: AMQPMessage
Bus->>Ev: case TYPE_FREE_CREDIT_INIT
Ev->>Ev: FreeCreditInitEntity
Ev->>Logic: handleFreeCreditInit
Logic->>Logic: freezeFirstRecharge + wallet RPC
结论:异步只发生在 MQ 传输层;单测应测 消息解析 → 路由 → 业务编排,而不是启动真实 event:bus 或 RabbitMQ。
项目里已有的做法(推荐延续)
1. 单元测试:直接测 Logic(主路径)
现有 tests/Unit/FreeCreditsHandleFreeCreditInitTest.php 已覆盖 free_credit_init 的业务结果,等价于事件消费后的效果:
FreeCreditsLogicHarness:注入userContext/activeConfig/walletServiceRecordingWalletService:记录freeCreditsFreeze,不发 HTTPFreeCreditsConfigStub:活动配置桩
断言示例(已有):
- 定格金额 =
balance_before_qf biz_id=free_credits_freeze:{orderId}balance_before_qf <= 0时不调用 freeze
运行:
cd slot_console && ./vendor/bin/phpunit --testsuite Unit
提现三类事件同理:tests/Integration/FreeCreditsLogicCashoutResultTest.php 直接调 handleFirstCashoutResult,不经过 EventBus。
2. 集成测试:需要 DB 时
FreeCreditsDbTestCase:RUN_DB_TESTS=1 才跑,默认事务回滚。
RUN_DB_TESTS=1 ./vendor/bin/phpunit --testsuite Integration --filter FreeCredits
建议的分层(按投入产出排序)
| 层级 | 测什么 | 是否必需 | 依赖 |
|---|---|---|---|
| A. Entity | FreeCreditInitEntity 字段映射、resolveWalletAmount() 回退 |
推荐 | 无 |
| B. Logic | handleFreeCreditInit / handleFirstCashoutResult 编排 |
已有,继续扩展 | Harness + RecordingWallet |
| C. Event | FreeCreditInitEvent::handle 把 MQBusEntity 转成 Logic 参数 |
可选 | 需可注入 Logic(见下) |
| D. EventBus | deal() 按 type 分发、失败 nack |
少量即可 | Mock AMQPMessage |
| E. E2E | 真 MQ + wallet 发消息 | 手工/脚本,非单测 | 环境 |
原则:B 覆盖 90% 风险;A 防 payload 字段错;C/D 防「路由写错 type」和「实体解析漏字段」。
可新增的测试示例
A. Entity 单测(纯数组 → 实体)
新建 tests/Unit/FreeCreditInitEntityTest.php:
$entity = new FreeCreditInitEntity([
'balance_before_qf' => 15000,
'wallet_amount' => 0,
'amount' => 5000,
'biz_id' => 'order_1',
]);
$this->assertSame(15000, $entity->balance_before_qf);
$this->assertSame(5000, $entity->resolveWalletAmount()); // wallet_amount 为 0 时回退 amount
对应生产代码:app/entity/mq/FreeCreditInitEntity.php。
B. 构造 MQ 载荷辅助方法
在 tests/Support/ 增加工厂,统一模拟 wallet 发出的 body:
public static function consoleBusMessage(int $uid, string $type, array $data): MQBusEntity
{
return new MQBusEntity(['uid' => $uid, 'type' => $type, 'data' => $data]);
}
// free_credit_init 示例
public static function freeCreditInitBus(int $uid, int $balanceBefore, int $walletAmount, string $bizId): MQBusEntity
{
return self::consoleBusMessage($uid, MQBusEntity::TYPE_FREE_CREDIT_INIT, [
'balance_before_qf' => $balanceBefore,
'wallet_amount' => $walletAmount,
'biz_id' => $bizId,
'source' => 'test',
'currency' => 'INR',
]);
}
与 WalletLogic::sendConsoleBus 字段保持一致。
C. Event 层单测(当前结构的限制)
FreeCreditInitEvent 内部写死 new FreeCreditsLogic(),无法在不改代码的情况下 mock Logic。
两种做法(二选一):
- 小重构(推荐):Event 构造函数注入
FreeCreditsLogic,单测传入FreeCreditsLogicHarness。 - 不测 Event:认为 Event 只有 5 行胶水,由 B 层保证;仅加 A 层测 data 解析。
若采用 1,示例:
$bus = ConsoleBusMessageFactory::freeCreditInitBus($uid, 15000, 5000, 'order_1');
$harness = (new FreeCreditsLogicHarness())->inject($context, $config, $wallet);
(new FreeCreditInitEvent($harness))->handle($bus);
$this->assertCount(1, $wallet->freezeCalls);
D. EventBus 路由单测(少量)
Mock PhpAmqpLib\Message\AMQPMessage:
$message = $this->createMock(AMQPMessage::class);
$message->method('getBody')->willReturn(json_encode([
'uid' => 90088002,
'type' => MQBusEntity::TYPE_FREE_CREDIT_INIT,
'data' => ['balance_before_qf' => 15000, 'wallet_amount' => 5000, 'biz_id' => 'x'],
]));
$message->expects($this->once())->method('ack'); // 成功应 ack
$bus = new EventBus();
// 若 Logic 仍 new 在 Event 内,需配合 C 的注入或接受集成测
$bus->deal($message);
nack 场景(EventBus.php L164-167):Logic 抛异常时,TYPE_FREE_CREDIT_INIT 应 nack(true) 且不 ack。可让 Harness 的 freeCreditsFreeze 抛异常,断言 $message->expects($this->once())->method('nack')->with(true)。
注意:EventBus 依赖多,路由测试宜 只测 switch 分支 + ack/nack,业务细节仍放 B。
四个 Free Credits 相关 type 怎么测
| MQ type(常量) | 单测重点 | 现有覆盖 |
|---|---|---|
TYPE_FREE_CREDIT_INIT |
定格金额、biz_id、幂等跳过 | FreeCreditsHandleFreeCreditInitTest |
TYPE_FREE_CREDITS_FIRST_CASHOUT_SUCCESS |
档位 completed、player 状态 | FreeCreditsLogicCashoutResultTest |
TYPE_FREE_CREDITS_FIRST_CASHOUT_FAIL |
失败回滚逻辑 | 可补 case |
TYPE_FREE_CREDITS_FIRST_CASHOUT_REJECTED |
rejected → ready | 已有 reject case |
EventBus 里前三类目前 直接调 Logic(未走独立 Event 类),单测继续打 Logic 即可;常量定义在 MQBusEntity。
不建议在单测里做的
- 启动
php webman event:bus或连接真实 RabbitMQ - 跨服务联调 wallet → console(留给脚本如
scripts/run_free_credits_first_freeze.php或手工验收) - 在单测里依赖
UserTagService::getByUid(用 Harness 注入 context)
推荐落地顺序(若你要补测试)
- 补
FreeCreditInitEntityTest(A) - 补
ConsoleBusMessageFactory(B 辅助) - 扩展
FreeCreditsHandleFreeCreditInitTest:无活动配置、已定格幂等、Logic 抛错 - (可选)
FreeCreditInitEvent注入 Logic + Event 单测(C) - (可选)
EventBusFreeCreditInitTest只测 ack/nack(D)
flowchart TD
subgraph unit [Unit 无 MQ]
E[EntityTest]
L[LogicHarnessTest]
end
subgraph optional [Optional]
Ev[EventTest]
Bus[EventBusRoutingTest]
end
subgraph integration [Integration RUN_DB_TESTS]
DB[DbTestCase]
end
E --> L
L --> Ev
Ev --> Bus
L --> DB