Files
cursor/plans/异步事件单元测试_0dbff6a0.plan.md
ray zhou f71a5c59af ok
2026-05-29 11:21:40 +08:00

8.9 KiB
Raw Permalink Blame History

name, overview, todos, isProject
name overview todos isProject
异步事件单元测试 结合 slot_console 现有 Free Credits 测试实践,说明异步 MQ 事件应分层测试:不直连 RabbitMQ优先测 Logic 编排,再补 Event 解析与 EventBus 路由;并给出可新增的示例用例结构。
id content status
entity-test 新增 FreeCreditInitEntityTest字段映射与 resolveWalletAmount 回退 completed
id content status
bus-factory 新增 tests/Support/ConsoleBusMessageFactory 统一构造 MQBusEntity 载荷 completed
id content status
extend-logic-tests 扩展 FreeCreditsHandleFreeCreditInitTest幂等、无配置、异常路径 completed
id content status
optional-event-inject 可选FreeCreditInitEvent 注入 Logic + Event 单测 completed
id content status
optional-eventbus-nack 可选EventBus deal 单测TYPE_FREE_CREDIT_INIT 成功 ack / 失败 nack completed
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业务结果,等价于事件消费后的效果:

断言示例(已有):

  • 定格金额 = 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 时

FreeCreditsDbTestCaseRUN_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::handleMQBusEntity 转成 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

两种做法(二选一):

  1. 小重构(推荐)Event 构造函数注入 FreeCreditsLogic,单测传入 FreeCreditsLogicHarness
  2. 不测 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-167Logic 抛异常时,TYPE_FREE_CREDIT_INITnack(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

推荐落地顺序(若你要补测试)

  1. FreeCreditInitEntityTestA
  2. ConsoleBusMessageFactoryB 辅助)
  3. 扩展 FreeCreditsHandleFreeCreditInitTest无活动配置、已定格幂等、Logic 抛错
  4. (可选)FreeCreditInitEvent 注入 Logic + Event 单测C
  5. (可选)EventBusFreeCreditInitTest 只测 ack/nackD
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