Files
cursor/plans/异步事件单元测试_0dbff6a0.plan.md
2026-05-21 18:16:26 +08:00

228 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: 异步事件单元测试
overview: 结合 slot_console 现有 Free Credits 测试实践,说明异步 MQ 事件应分层测试:不直连 RabbitMQ优先测 Logic 编排,再补 Event 解析与 EventBus 路由;并给出可新增的示例用例结构。
todos:
- id: entity-test
content: 新增 FreeCreditInitEntityTest字段映射与 resolveWalletAmount 回退
status: completed
- id: bus-factory
content: 新增 tests/Support/ConsoleBusMessageFactory 统一构造 MQBusEntity 载荷
status: completed
- id: extend-logic-tests
content: 扩展 FreeCreditsHandleFreeCreditInitTest幂等、无配置、异常路径
status: completed
- id: optional-event-inject
content: 可选FreeCreditInitEvent 注入 Logic + Event 单测
status: completed
- id: optional-eventbus-nack
content: 可选EventBus deal 单测TYPE_FREE_CREDIT_INIT 成功 ack / 失败 nack
status: completed
isProject: false
---
# 异步事件单元测试写法slot_console
## 当前架构
```mermaid
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`](/Users/ray/Documents/project/www/slot/slot_console/tests/Unit/FreeCreditsHandleFreeCreditInitTest.php) 已覆盖 `free_credit_init` 的**业务结果**,等价于事件消费后的效果:
- [`FreeCreditsLogicHarness`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/FreeCreditsLogicHarness.php):注入 `userContext` / `activeConfig` / `walletService`
- [`RecordingWalletService`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/RecordingWalletService.php):记录 `freeCreditsFreeze`,不发 HTTP
- [`FreeCreditsConfigStub`](/Users/ray/Documents/project/www/slot/slot_console/tests/Support/FreeCreditsConfigStub.php):活动配置桩
断言示例(已有):
- 定格金额 = `balance_before_qf`
- `biz_id` = `free_credits_freeze:{orderId}`
- `balance_before_qf <= 0` 时不调用 freeze
**运行**
```bash
cd slot_console && ./vendor/bin/phpunit --testsuite Unit
```
提现三类事件同理:[`tests/Integration/FreeCreditsLogicCashoutResultTest.php`](/Users/ray/Documents/project/www/slot/slot_console/tests/Integration/FreeCreditsLogicCashoutResultTest.php) 直接调 `handleFirstCashoutResult`,不经过 EventBus。
### 2. 集成测试:需要 DB 时
[`FreeCreditsDbTestCase`](/Users/ray/Documents/project/www/slot/slot_console/tests/Integration/FreeCreditsDbTestCase.php)`RUN_DB_TESTS=1` 才跑,默认事务回滚。
```bash
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`
```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`](/Users/ray/Documents/project/www/slot/slot_console/app/entity/mq/FreeCreditInitEntity.php)。
### B. 构造 MQ 载荷辅助方法
`tests/Support/` 增加工厂,统一模拟 wallet 发出的 body
```php
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`](/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php) 字段保持一致。
### C. Event 层单测(当前结构的限制)
[`FreeCreditInitEvent`](/Users/ray/Documents/project/www/slot/slot_console/app/command/event/FreeCreditInitEvent.php) 内部写死 `new FreeCreditsLogic()`**无法在不改代码的情况下 mock Logic**。
两种做法(二选一):
1. **小重构(推荐)**Event 构造函数注入 `FreeCreditsLogic`,单测传入 `FreeCreditsLogicHarness`
2. **不测 Event**:认为 Event 只有 5 行胶水,由 B 层保证;仅加 A 层测 data 解析。
若采用 1示例
```php
$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`
```php
$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](/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php)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`](/Users/ray/Documents/project/www/slot/slot_console/app/entity/mq/MQBusEntity.php)。
---
## 不建议在单测里做的
- 启动 `php webman event:bus` 或连接真实 RabbitMQ
- 跨服务联调 wallet → console留给脚本如 `scripts/run_free_credits_first_freeze.php` 或手工验收)
- 在单测里依赖 `UserTagService::getByUid`(用 Harness 注入 context
---
## 推荐落地顺序(若你要补测试)
1.`FreeCreditInitEntityTest`A
2.`ConsoleBusMessageFactory`B 辅助)
3. 扩展 `FreeCreditsHandleFreeCreditInitTest`无活动配置、已定格幂等、Logic 抛错
4. (可选)`FreeCreditInitEvent` 注入 Logic + Event 单测C
5. (可选)`EventBusFreeCreditInitTest` 只测 ack/nackD
```mermaid
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
```