test(group_horse_racing): 重构模拟赛马测试以使用依赖注入
重构测试命令 `测试模拟赛马`,使用模拟依赖(内存存储、积分服务)替换真实组件,避免测试产生数据库副作用。 新增模拟类 `_FakeBot`、`_InMemoryRoomStore`、`_InMemoryPointsService` 和 `_NoopMessageService`,并临时替换模块中的全局依赖以执行完整比赛流程。 验证开赛消息、回合进度、结束结算、赛史保存及积分奖励调用的正确性,确保核心逻辑在隔离环境中正常工作。
This commit is contained in:
@@ -3,9 +3,13 @@ from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent, PrivateMe
|
||||
|
||||
from . import plugin_config as config
|
||||
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
|
||||
from datetime import datetime
|
||||
|
||||
from . import commands as commands_mod
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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()
|
||||
async def handle_test_simulate_race(bot: Bot, event: Event):
|
||||
if not await check_tester(event):
|
||||
@@ -144,65 +207,94 @@ async def handle_test_simulate_race(bot: Bot, event: Event):
|
||||
async with lock:
|
||||
race_engine.stop_race(scope)
|
||||
room_store.delete_room(scope)
|
||||
room = room_store.create_room(scope)
|
||||
|
||||
horse_names = _generate_random_horse_names(8)
|
||||
for idx, horse_name in enumerate(horse_names, start=1):
|
||||
owner_id = f"sim_user_{idx}"
|
||||
room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING)
|
||||
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
|
||||
|
||||
room.state = RoomState.RUNNING
|
||||
fake_room_store = _InMemoryRoomStore()
|
||||
fake_points_service = _InMemoryPointsService()
|
||||
fake_message_service = _NoopMessageService()
|
||||
fake_bot = _FakeBot()
|
||||
|
||||
progress_samples: list[str] = []
|
||||
last_progress = ""
|
||||
finished = []
|
||||
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
|
||||
|
||||
for _ in range(1_000):
|
||||
finished = race_engine.tick(room)
|
||||
last_progress = race_engine.format_progress(room)
|
||||
if len(progress_samples) < 3:
|
||||
progress_samples.append(last_progress)
|
||||
if finished:
|
||||
break
|
||||
room = fake_room_store.create_room(scope)
|
||||
horse_names = _generate_random_horse_names(8)
|
||||
for idx, horse_name in enumerate(horse_names, start=1):
|
||||
owner_id = f"sim_user_{idx}"
|
||||
room.horses[horse_name] = Horse(owner_id=owner_id, name=horse_name, state=HorseState.RACING)
|
||||
|
||||
if not finished:
|
||||
race_engine.stop_race(scope)
|
||||
room_store.delete_room(scope)
|
||||
await test_simulate_race_cmd.finish("模拟失败:超过最大回合仍未结束")
|
||||
return
|
||||
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))
|
||||
|
||||
champion = race_engine.determine_champion(finished)
|
||||
room.champion_name = champion.name
|
||||
room.state = RoomState.FINISHED
|
||||
room.state = RoomState.WAITING
|
||||
|
||||
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
|
||||
for horse in room.horses.values():
|
||||
horse.state = HorseState.RACING
|
||||
|
||||
missing_names = [name for name in room.horses.keys() if name not in last_progress]
|
||||
if missing_names:
|
||||
race_engine.stop_race(scope)
|
||||
room_store.delete_room(scope)
|
||||
await test_simulate_race_cmd.finish(f"模拟失败:进度渲染缺少马名:{', '.join(missing_names)}")
|
||||
return
|
||||
start_task = asyncio.create_task(commands_mod.run_race_with_settlement(fake_bot, room, scope))
|
||||
commands_mod.race_engine.register_task(scope, start_task)
|
||||
await start_task
|
||||
|
||||
race_engine.stop_race(scope)
|
||||
room_store.delete_room(scope)
|
||||
messages = [m.get("message", "") for m in fake_bot.messages]
|
||||
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])
|
||||
await test_simulate_race_cmd.finish(
|
||||
"\n".join(
|
||||
[
|
||||
"模拟赛马完成",
|
||||
f"参赛马匹:{', '.join(horse_names)}",
|
||||
f"冠军:{room.champion_name}",
|
||||
f"总回合:{room.tick_count}",
|
||||
"进度片段:",
|
||||
sample_block,
|
||||
]
|
||||
)
|
||||
)
|
||||
if "比赛开始!" not in messages[0]:
|
||||
await test_simulate_race_cmd.finish("完全模拟失败:未发送开赛消息")
|
||||
return
|
||||
|
||||
if not any("【第" in msg and "回合】" in msg for msg in messages):
|
||||
await test_simulate_race_cmd.finish("完全模拟失败:未发送回合进度消息")
|
||||
return
|
||||
|
||||
result_msg = messages[-1]
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user