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)
original_room_store = commands_mod.room_store
original_points_service = commands_mod.points_service
original_message_service = commands_mod.message_service
original_tick_interval = commands_mod.config.RACE_TICK_INTERVAL
fake_room_store = _InMemoryRoomStore()
fake_points_service = _InMemoryPointsService()
fake_message_service = _NoopMessageService()
fake_bot = _FakeBot()
try:
commands_mod.room_store = fake_room_store
commands_mod.points_service = fake_points_service
commands_mod.message_service = fake_message_service
commands_mod.config.RACE_TICK_INTERVAL = 0
room = fake_room_store.create_room(scope)
horse_names = _generate_random_horse_names(8) horse_names = _generate_random_horse_names(8)
for idx, horse_name in enumerate(horse_names, start=1): for idx, horse_name in enumerate(horse_names, start=1):
owner_id = f"sim_user_{idx}" owner_id = f"sim_user_{idx}"
room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING) room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING)
room.state = RoomState.RUNNING bet_amount = max(commands_mod.config.MIN_BET, 10)
room.bets.append(Bet(user_id="bettor_1", horse_name=horse_names[0], amount=bet_amount))
room.bets.append(Bet(user_id="bettor_2", horse_name=horse_names[1], amount=bet_amount * 2))
progress_samples: list[str] = [] room.state = RoomState.WAITING
last_progress = ""
finished = []
for _ in range(1_000): for horse in room.horses.values():
finished = race_engine.tick(room) horse.state = HorseState.RACING
last_progress = race_engine.format_progress(room)
if len(progress_samples) < 3:
progress_samples.append(last_progress)
if finished:
break
if not finished: start_task = asyncio.create_task(commands_mod.run_race_with_settlement(fake_bot, room, scope))
race_engine.stop_race(scope) commands_mod.race_engine.register_task(scope, start_task)
room_store.delete_room(scope) await start_task
await test_simulate_race_cmd.finish("模拟失败:超过最大回合仍未结束")
messages = [m.get("message", "") for m in fake_bot.messages]
if not messages:
await test_simulate_race_cmd.finish("完全模拟失败:未捕获到任何消息")
return return
champion = race_engine.determine_champion(finished) if "比赛开始!" not in messages[0]:
room.champion_name = champion.name await test_simulate_race_cmd.finish("完全模拟失败:未发送开赛消息")
room.state = RoomState.FINISHED
expected_lines = 1 + len(room.horses)
actual_lines = len(last_progress.splitlines())
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 return
missing_names = [name for name in room.horses.keys() if name not in last_progress] if not any("【第" in msg and "回合】" in msg for msg in messages):
if missing_names: await test_simulate_race_cmd.finish("完全模拟失败:未发送回合进度消息")
race_engine.stop_race(scope)
room_store.delete_room(scope)
await test_simulate_race_cmd.finish(f"模拟失败:进度渲染缺少马名:{', '.join(missing_names)}")
return return
race_engine.stop_race(scope) result_msg = messages[-1]
room_store.delete_room(scope) 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
sample_block = "\n\n".join(progress_samples[-2:] + [last_progress] if len(progress_samples) >= 2 else [last_progress])
await test_simulate_race_cmd.finish( await test_simulate_race_cmd.finish(
"\n".join( "\n".join(
[ [
"模拟赛马完成", "完全模拟赛马完成(无真实积分/数据库副作用)",
f"参赛马匹:{', '.join(horse_names)}", f"参赛马匹:{', '.join(horse_names)}",
f"冠军:{room.champion_name}", f"冠军:{saved.champion_name}(马主:{saved.champion_owner}",
f"总回合:{room.tick_count}", f"总回合:{saved.duration_ticks}",
"进度片段:", f"消息条数:{len(messages)}(开赛/进度/结算均已覆盖)",
sample_block, 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