test(group_horse_racing): 重构模拟赛马测试以使用依赖注入

重构测试命令 `测试模拟赛马`,使用模拟依赖(内存存储、积分服务)替换真实组件,避免测试产生数据库副作用。
新增模拟类 `_FakeBot`、`_InMemoryRoomStore`、`_InMemoryPointsService` 和 `_NoopMessageService`,并临时替换模块中的全局依赖以执行完整比赛流程。
验证开赛消息、回合进度、结束结算、赛史保存及积分奖励调用的正确性,确保核心逻辑在隔离环境中正常工作。
This commit is contained in:
2026-04-04 21:21:36 +08:00
parent c0798d127b
commit 1c9d964747

View File

@@ -3,9 +3,13 @@ from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent, PrivateMe
from . import plugin_config as config from . import plugin_config as config
from .commands import get_scope, check_access, room_store, points_service, race_engine from .commands import get_scope, check_access, room_store, points_service, race_engine
from .models import Horse, HorseState, RoomState from .models import Horse, HorseState, RoomState, Bet, RaceResult
import asyncio
import random import random
from datetime import datetime
from . import commands as commands_mod
async def check_tester(event: Event) -> bool: async def check_tester(event: Event) -> bool:
@@ -132,6 +136,65 @@ def _generate_random_horse_names(count: int) -> list[str]:
test_simulate_race_cmd = on_command("测试模拟赛马", priority=5) test_simulate_race_cmd = on_command("测试模拟赛马", priority=5)
class _FakeBot:
def __init__(self):
self.messages: list[dict] = []
self._next_message_id = 1
async def send_msg(self, **kwargs):
self.messages.append(dict(kwargs))
message_id = str(self._next_message_id)
self._next_message_id += 1
return message_id
class _InMemoryRoomStore:
def __init__(self):
self.rooms: dict[str, "commands_mod.Room"] = {}
self.saved_results: list[RaceResult] = []
def get_room(self, scope: str):
return self.rooms.get(scope)
def create_room(self, scope: str):
room = commands_mod.Room(scope=scope)
self.rooms[scope] = room
return room
def delete_room(self, scope: str):
if scope in self.rooms:
del self.rooms[scope]
def save_race_result(self, result: RaceResult):
self.saved_results.append(result)
class _InMemoryPointsService:
def __init__(self):
self.calls: list[tuple[str, dict]] = []
async def reward_champion(self, user_id: str):
self.calls.append(("reward_champion", {"user_id": user_id}))
return True, 0
async def reward_participant(self, user_id: str):
self.calls.append(("reward_participant", {"user_id": user_id}))
return True, 0
async def payout_winnings(self, user_id: str, amount: int, odds: float):
self.calls.append(("payout_winnings", {"user_id": user_id, "amount": amount, "odds": odds}))
return True, 0
async def refund_bet_points(self, user_id: str, amount: int, reason: str = "比赛中断退还"):
self.calls.append(("refund_bet_points", {"user_id": user_id, "amount": amount, "reason": reason}))
return True, 0
class _NoopMessageService:
def clear_pending_recalls(self, scope: str):
return
@test_simulate_race_cmd.handle() @test_simulate_race_cmd.handle()
async def handle_test_simulate_race(bot: Bot, event: Event): async def handle_test_simulate_race(bot: Bot, event: Event):
if not await check_tester(event): if not await check_tester(event):
@@ -144,65 +207,94 @@ async def handle_test_simulate_race(bot: Bot, event: Event):
async with lock: async with lock:
race_engine.stop_race(scope) race_engine.stop_race(scope)
room_store.delete_room(scope) room_store.delete_room(scope)
room = room_store.create_room(scope)
horse_names = _generate_random_horse_names(8) original_room_store = commands_mod.room_store
for idx, horse_name in enumerate(horse_names, start=1): original_points_service = commands_mod.points_service
owner_id = f"sim_user_{idx}" original_message_service = commands_mod.message_service
room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING) original_tick_interval = commands_mod.config.RACE_TICK_INTERVAL
room.state = RoomState.RUNNING fake_room_store = _InMemoryRoomStore()
fake_points_service = _InMemoryPointsService()
fake_message_service = _NoopMessageService()
fake_bot = _FakeBot()
progress_samples: list[str] = [] try:
last_progress = "" commands_mod.room_store = fake_room_store
finished = [] commands_mod.points_service = fake_points_service
commands_mod.message_service = fake_message_service
commands_mod.config.RACE_TICK_INTERVAL = 0
for _ in range(1_000): room = fake_room_store.create_room(scope)
finished = race_engine.tick(room) horse_names = _generate_random_horse_names(8)
last_progress = race_engine.format_progress(room) for idx, horse_name in enumerate(horse_names, start=1):
if len(progress_samples) < 3: owner_id = f"sim_user_{idx}"
progress_samples.append(last_progress) room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING)
if finished:
break
if not finished: bet_amount = max(commands_mod.config.MIN_BET, 10)
race_engine.stop_race(scope) room.bets.append(Bet(user_id="bettor_1", horse_name=horse_names[0], amount=bet_amount))
room_store.delete_room(scope) room.bets.append(Bet(user_id="bettor_2", horse_name=horse_names[1], amount=bet_amount * 2))
await test_simulate_race_cmd.finish("模拟失败:超过最大回合仍未结束")
return
champion = race_engine.determine_champion(finished) room.state = RoomState.WAITING
room.champion_name = champion.name
room.state = RoomState.FINISHED
expected_lines = 1 + len(room.horses) for horse in room.horses.values():
actual_lines = len(last_progress.splitlines()) horse.state = HorseState.RACING
if actual_lines != expected_lines:
race_engine.stop_race(scope)
room_store.delete_room(scope)
await test_simulate_race_cmd.finish(f"模拟失败:进度渲染行数不匹配(期望{expected_lines},实际{actual_lines}")
return
missing_names = [name for name in room.horses.keys() if name not in last_progress] start_task = asyncio.create_task(commands_mod.run_race_with_settlement(fake_bot, room, scope))
if missing_names: commands_mod.race_engine.register_task(scope, start_task)
race_engine.stop_race(scope) await start_task
room_store.delete_room(scope)
await test_simulate_race_cmd.finish(f"模拟失败:进度渲染缺少马名:{', '.join(missing_names)}")
return
race_engine.stop_race(scope) messages = [m.get("message", "") for m in fake_bot.messages]
room_store.delete_room(scope) if not messages:
await test_simulate_race_cmd.finish("完全模拟失败:未捕获到任何消息")
return
sample_block = "\n\n".join(progress_samples[-2:] + [last_progress] if len(progress_samples) >= 2 else [last_progress]) if "比赛开始!" not in messages[0]:
await test_simulate_race_cmd.finish( await test_simulate_race_cmd.finish("完全模拟失败:未发送开赛消息")
"\n".join( return
[
"模拟赛马完成", if not any("【第" in msg and "回合】" in msg for msg in messages):
f"参赛马匹:{', '.join(horse_names)}", await test_simulate_race_cmd.finish("完全模拟失败:未发送回合进度消息")
f"冠军:{room.champion_name}", return
f"总回合:{room.tick_count}",
"进度片段:", result_msg = messages[-1]
sample_block, if "比赛结束!冠军:" not in result_msg:
] await test_simulate_race_cmd.finish("完全模拟失败:未发送结束结算消息")
) return
)
if not fake_room_store.saved_results:
await test_simulate_race_cmd.finish("完全模拟失败:未写入赛史结果(内存)")
return
saved = fake_room_store.saved_results[-1]
if saved.champion_name not in room.horses:
await test_simulate_race_cmd.finish("完全模拟失败:赛史冠军不在参赛马匹中")
return
champion_owner_id = room.horses[saved.champion_name].owner_id
reward_champion_calls = [c for c in fake_points_service.calls if c[0] == "reward_champion"]
if not reward_champion_calls or reward_champion_calls[0][1]["user_id"] != champion_owner_id:
await test_simulate_race_cmd.finish("完全模拟失败:未正确发放冠军奖励(内存记录)")
return
participant_calls = [c for c in fake_points_service.calls if c[0] == "reward_participant"]
if len(participant_calls) != max(0, len(room.horses) - 1):
await test_simulate_race_cmd.finish("完全模拟失败:参赛奖励次数不匹配(内存记录)")
return
await test_simulate_race_cmd.finish(
"\n".join(
[
"完全模拟赛马完成(无真实积分/数据库副作用)",
f"参赛马匹:{', '.join(horse_names)}",
f"冠军:{saved.champion_name}(马主:{saved.champion_owner}",
f"总回合:{saved.duration_ticks}",
f"消息条数:{len(messages)}(开赛/进度/结算均已覆盖)",
f"结算调用:{len(fake_points_service.calls)}(冠军/参赛/下注派奖)",
]
)
)
finally:
commands_mod.room_store = original_room_store
commands_mod.points_service = original_points_service
commands_mod.message_service = original_message_service
commands_mod.config.RACE_TICK_INTERVAL = original_tick_interval