test: enhance test framework with comprehensive fixtures and mocks (#5354)

* test: enhance test framework with comprehensive fixtures and mocks

- Add shared mock builders for aiocqhttp, discord, telegram
- Add test helpers for platform configs and mock objects
- Expand conftest.py with test profile support
- Update coverage test workflow configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(tests): 移动并重构模拟 LLM 响应和消息组件函数

* fix(tests): 优化 pytest_runtest_setup 中的标记检查逻辑

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
whatevertogo
2026-02-23 23:35:15 +08:00
committed by GitHub
parent 28bfb3b8b2
commit 7b731ebda8
12 changed files with 1259 additions and 1 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
mkdir -p data/temp
export TESTING=true
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
pytest --cov=. -v -o log_cli=true -o log_level=DEBUG
pytest --cov=astrbot -v -o log_cli=true -o log_level=DEBUG
- name: Upload results to Codecov
uses: codecov/codecov-action@v5
+381
View File
@@ -0,0 +1,381 @@
"""
AstrBot 测试配置
提供共享的 pytest fixtures 和测试工具。
"""
import json
import os
import sys
from asyncio import Queue
from pathlib import Path
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
import pytest_asyncio
# 使用 tests/fixtures/helpers.py 中的共享工具函数,避免重复定义
from tests.fixtures.helpers import create_mock_llm_response, create_mock_message_component
# 将项目根目录添加到 sys.path
PROJECT_ROOT = Path(__file__).parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
# 设置测试环境变量
os.environ.setdefault("TESTING", "true")
os.environ.setdefault("ASTRBOT_TEST_MODE", "true")
# ============================================================
# 测试收集和排序
# ============================================================
def pytest_collection_modifyitems(session, config, items): # noqa: ARG001
"""重新排序测试:单元测试优先,集成测试在后。"""
unit_tests = []
integration_tests = []
deselected = []
profile = config.getoption("--test-profile") or os.environ.get(
"ASTRBOT_TEST_PROFILE", "all"
)
for item in items:
item_path = Path(str(item.path))
is_integration = "integration" in item_path.parts
if is_integration:
if item.get_closest_marker("integration") is None:
item.add_marker(pytest.mark.integration)
item.add_marker(pytest.mark.tier_d)
integration_tests.append(item)
else:
if item.get_closest_marker("unit") is None:
item.add_marker(pytest.mark.unit)
if any(
item.get_closest_marker(marker) is not None
for marker in ("platform", "provider", "slow")
):
item.add_marker(pytest.mark.tier_c)
unit_tests.append(item)
# 单元测试 -> 集成测试
ordered_items = unit_tests + integration_tests
if profile == "blocking":
selected_items = []
for item in ordered_items:
if item.get_closest_marker("tier_c") or item.get_closest_marker("tier_d"):
deselected.append(item)
else:
selected_items.append(item)
if deselected:
config.hook.pytest_deselected(items=deselected)
items[:] = selected_items
return
items[:] = ordered_items
def pytest_addoption(parser):
"""增加测试执行档位选择。"""
parser.addoption(
"--test-profile",
action="store",
default=None,
choices=["all", "blocking"],
help="Select test profile. 'blocking' excludes auto-classified tier_c/tier_d tests.",
)
def pytest_configure(config):
"""注册自定义标记。"""
config.addinivalue_line("markers", "unit: 单元测试")
config.addinivalue_line("markers", "integration: 集成测试")
config.addinivalue_line("markers", "slow: 慢速测试")
config.addinivalue_line("markers", "platform: 平台适配器测试")
config.addinivalue_line("markers", "provider: LLM Provider 测试")
config.addinivalue_line("markers", "db: 数据库相关测试")
config.addinivalue_line("markers", "tier_c: C-tier tests (optional / non-blocking)")
config.addinivalue_line("markers", "tier_d: D-tier tests (extended / integration)")
# ============================================================
# 临时目录和文件 Fixtures
# ============================================================
@pytest.fixture
def temp_dir(tmp_path: Path) -> Path:
"""创建临时目录用于测试。"""
return tmp_path
@pytest.fixture
def event_queue() -> Queue:
"""Create a shared asyncio queue fixture for tests."""
return Queue()
@pytest.fixture
def platform_settings() -> dict:
"""Create a shared empty platform settings fixture for adapter tests."""
return {}
@pytest.fixture
def temp_data_dir(temp_dir: Path) -> Path:
"""创建模拟的 data 目录结构。"""
data_dir = temp_dir / "data"
data_dir.mkdir()
# 创建必要的子目录
(data_dir / "config").mkdir()
(data_dir / "plugins").mkdir()
(data_dir / "temp").mkdir()
(data_dir / "attachments").mkdir()
return data_dir
@pytest.fixture
def temp_config_file(temp_data_dir: Path) -> Path:
"""创建临时配置文件。"""
config_path = temp_data_dir / "config" / "cmd_config.json"
default_config = {
"provider": [],
"platform": [],
"provider_settings": {},
"default_personality": None,
"timezone": "Asia/Shanghai",
}
config_path.write_text(json.dumps(default_config, indent=2), encoding="utf-8")
return config_path
@pytest.fixture
def temp_db_file(temp_data_dir: Path) -> Path:
"""创建临时数据库文件路径。"""
return temp_data_dir / "test.db"
# ============================================================
# Mock Fixtures
# ============================================================
@pytest.fixture
def mock_provider():
"""创建模拟的 Provider。"""
provider = MagicMock()
provider.provider_config = {
"id": "test-provider",
"type": "openai_chat_completion",
"model": "gpt-4o-mini",
}
provider.get_model = MagicMock(return_value="gpt-4o-mini")
provider.text_chat = AsyncMock()
provider.text_chat_stream = AsyncMock()
provider.terminate = AsyncMock()
return provider
@pytest.fixture
def mock_platform():
"""创建模拟的 Platform。"""
platform = MagicMock()
platform.platform_name = "test_platform"
platform.platform_meta = MagicMock()
platform.platform_meta.support_proactive_message = False
platform.send_message = AsyncMock()
platform.terminate = AsyncMock()
return platform
@pytest.fixture
def mock_conversation():
"""创建模拟的 Conversation。"""
from astrbot.core.db.po import ConversationV2
return ConversationV2(
conversation_id="test-conv-id",
platform_id="test_platform",
user_id="test_user",
content=[],
persona_id=None,
)
@pytest.fixture
def mock_event():
"""创建模拟的 AstrMessageEvent。"""
event = MagicMock()
event.unified_msg_origin = "test_umo"
event.session_id = "test_session"
event.message_str = "Hello, world!"
event.message_obj = MagicMock()
event.message_obj.message = []
event.message_obj.sender = MagicMock()
event.message_obj.sender.user_id = "test_user"
event.message_obj.sender.nickname = "Test User"
event.message_obj.group_id = None
event.message_obj.group = None
event.get_platform_name = MagicMock(return_value="test_platform")
event.get_platform_id = MagicMock(return_value="test_platform")
event.get_group_id = MagicMock(return_value=None)
event.get_extra = MagicMock(return_value=None)
event.set_extra = MagicMock()
event.trace = MagicMock()
event.platform_meta = MagicMock()
event.platform_meta.support_proactive_message = False
return event
# ============================================================
# 配置 Fixtures
# ============================================================
@pytest.fixture
def astrbot_config(temp_config_file: Path):
"""创建 AstrBotConfig 实例。"""
from astrbot.core.config.astrbot_config import AstrBotConfig
config = AstrBotConfig()
config._config_path = str(temp_config_file) # noqa: SLF001
return config
@pytest.fixture
def main_agent_build_config():
"""创建 MainAgentBuildConfig 实例。"""
from astrbot.core.astr_main_agent import MainAgentBuildConfig
return MainAgentBuildConfig(
tool_call_timeout=60,
tool_schema_mode="full",
provider_wake_prefix="",
streaming_response=True,
sanitize_context_by_modalities=False,
kb_agentic_mode=False,
file_extract_enabled=False,
context_limit_reached_strategy="truncate_by_turns",
llm_safety_mode=True,
computer_use_runtime="local",
add_cron_tools=True,
)
# ============================================================
# 数据库 Fixtures
# ============================================================
@pytest_asyncio.fixture
async def temp_db(temp_db_file: Path):
"""创建临时数据库实例。"""
from astrbot.core.db.sqlite import SQLiteDatabase
db = SQLiteDatabase(str(temp_db_file))
try:
yield db
finally:
await db.engine.dispose()
if temp_db_file.exists():
temp_db_file.unlink()
# ============================================================
# Context Fixtures
# ============================================================
@pytest_asyncio.fixture
async def mock_context(
astrbot_config,
temp_db,
mock_provider,
mock_platform,
):
"""创建模拟的插件上下文。"""
from asyncio import Queue
from astrbot.core.star.context import Context
event_queue = Queue()
provider_manager = MagicMock()
provider_manager.get_using_provider = MagicMock(return_value=mock_provider)
provider_manager.get_provider_by_id = MagicMock(return_value=mock_provider)
platform_manager = MagicMock()
conversation_manager = MagicMock()
message_history_manager = MagicMock()
persona_manager = MagicMock()
persona_manager.personas_v3 = []
astrbot_config_mgr = MagicMock()
knowledge_base_manager = MagicMock()
cron_manager = MagicMock()
subagent_orchestrator = None
context = Context(
event_queue,
astrbot_config,
temp_db,
provider_manager,
platform_manager,
conversation_manager,
message_history_manager,
persona_manager,
astrbot_config_mgr,
knowledge_base_manager,
cron_manager,
subagent_orchestrator,
)
return context
# ============================================================
# Provider Request Fixtures
# ============================================================
@pytest.fixture
def provider_request():
"""创建 ProviderRequest 实例。"""
from astrbot.core.provider.entities import ProviderRequest
return ProviderRequest(
prompt="Hello",
session_id="test_session",
image_urls=[],
contexts=[],
system_prompt="You are a helpful assistant.",
)
# ============================================================
# 跳过条件
# ============================================================
def pytest_runtest_setup(item):
"""在测试运行前检查跳过条件。"""
# 跳过需要 API Key 但未设置的 Provider 测试
if item.get_closest_marker("provider"):
if not os.environ.get("TEST_PROVIDER_API_KEY"):
pytest.skip("TEST_PROVIDER_API_KEY not set")
# 跳过需要特定平台的测试
if item.get_closest_marker("platform"):
required_platform = None
marker = item.get_closest_marker("platform")
if marker and marker.args:
required_platform = marker.args[0]
if required_platform and not os.environ.get(
f"TEST_{required_platform.upper()}_ENABLED"
):
pytest.skip(f"TEST_{required_platform.upper()}_ENABLED not set")
+64
View File
@@ -0,0 +1,64 @@
"""
AstrBot 测试数据
此目录存放测试用的静态数据和配置文件。
目录结构:
- fixtures/
├── configs/ # 测试配置文件
├── messages/ # 测试消息数据
├── plugins/ # 测试插件
├── knowledge_base/ # 测试知识库数据
├── mocks/ # Mock 模块
└── helpers.py # 辅助函数
"""
import json
from pathlib import Path
from .helpers import (
NoopAwaitable,
create_mock_discord_attachment,
create_mock_discord_channel,
create_mock_discord_user,
create_mock_file,
create_mock_llm_response,
create_mock_message_component,
create_mock_update,
make_platform_config,
)
FIXTURES_DIR = Path(__file__).parent
def load_fixture(filename: str) -> dict:
"""加载 JSON 格式的测试数据。"""
filepath = FIXTURES_DIR / filename
if not filepath.exists():
raise FileNotFoundError(f"Fixture not found: {filepath}")
return json.loads(filepath.read_text(encoding="utf-8"))
def get_fixture_path(filename: str) -> Path:
"""获取测试数据文件路径。"""
filepath = FIXTURES_DIR / filename
if not filepath.exists():
raise FileNotFoundError(f"Fixture not found: {filepath}")
return filepath
__all__ = [
"FIXTURES_DIR",
"load_fixture",
"get_fixture_path",
# 辅助函数
"NoopAwaitable",
"make_platform_config",
"create_mock_update",
"create_mock_file",
"create_mock_discord_attachment",
"create_mock_discord_user",
"create_mock_discord_channel",
"create_mock_message_component",
"create_mock_llm_response",
]
+21
View File
@@ -0,0 +1,21 @@
{
"provider": [
{
"id": "test-openai",
"type": "openai_chat_completion",
"model": "gpt-4o-mini",
"key": ["test-key"]
}
],
"platform": [],
"provider_settings": {
"default_personality": null,
"prompt_prefix": "",
"image_caption_provider_id": "",
"datetime_system_prompt": true,
"identifier": true,
"group_name_display": true
},
"default_personality": null,
"timezone": "Asia/Shanghai"
}
+332
View File
@@ -0,0 +1,332 @@
"""测试辅助函数和工具类。
提供统一的测试辅助工具,减少测试代码重复。
"""
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from astrbot.core.message.components import BaseMessageComponent
class NoopAwaitable:
"""可等待的空操作对象。
用于 mock 需要返回 awaitable 对象的方法。
"""
def __await__(self):
if False:
yield
return None
# ============================================================
# 平台配置工厂
# ============================================================
def make_platform_config(platform_type: str, **kwargs) -> dict:
"""平台配置工厂函数。
Args:
platform_type: 平台类型 (telegram, discord, aiocqhttp 等)
**kwargs: 覆盖默认配置的字段
Returns:
dict: 平台配置字典
"""
configs = {
"telegram": {
"id": "test_telegram",
"telegram_token": "test_token_123",
"telegram_api_base_url": "https://api.telegram.org/bot",
"telegram_file_base_url": "https://api.telegram.org/file/bot",
"telegram_command_register": True,
"telegram_command_auto_refresh": True,
"telegram_command_register_interval": 300,
"telegram_media_group_timeout": 2.5,
"telegram_media_group_max_wait": 10.0,
"start_message": "Welcome to AstrBot!",
},
"discord": {
"id": "test_discord",
"discord_token": "test_token_123",
"discord_proxy": None,
"discord_command_register": True,
"discord_guild_id_for_debug": None,
"discord_activity_name": "Playing AstrBot",
},
"aiocqhttp": {
"id": "test_aiocqhttp",
"ws_reverse_host": "0.0.0.0",
"ws_reverse_port": 6199,
"ws_reverse_token": "test_token",
},
"webchat": {
"id": "test_webchat",
},
"wecom": {
"id": "test_wecom",
"wecom_corpid": "test_corpid",
"wecom_secret": "test_secret",
},
}
config = configs.get(platform_type, {"id": f"test_{platform_type}"}).copy()
config.update(kwargs)
return config
# ============================================================
# Telegram 辅助函数
# ============================================================
def create_mock_update(
message_text: str | None = "Hello World",
chat_type: str = "private",
chat_id: int = 123456789,
user_id: int = 987654321,
username: str = "test_user",
message_id: int = 1,
media_group_id: str | None = None,
photo: list | None = None,
video: MagicMock | None = None,
document: MagicMock | None = None,
voice: MagicMock | None = None,
sticker: MagicMock | None = None,
reply_to_message: MagicMock | None = None,
caption: str | None = None,
entities: list | None = None,
caption_entities: list | None = None,
message_thread_id: int | None = None,
is_topic_message: bool = False,
):
"""创建模拟的 Telegram Update 对象。
Args:
message_text: 消息文本
chat_type: 聊天类型
chat_id: 聊天 ID
user_id: 用户 ID
username: 用户名
message_id: 消息 ID
media_group_id: 媒体组 ID
photo: 图片列表
video: 视频对象
document: 文档对象
voice: 语音对象
sticker: 贴纸对象
reply_to_message: 回复的消息
caption: 说明文字
entities: 实体列表
caption_entities: 说明实体列表
message_thread_id: 消息线程 ID
is_topic_message: 是否为主题消息
Returns:
MagicMock: 模拟的 Update 对象
"""
update = MagicMock()
update.update_id = 1
# Create message mock
message = MagicMock()
message.message_id = message_id
message.chat = MagicMock()
message.chat.id = chat_id
message.chat.type = chat_type
message.message_thread_id = message_thread_id
message.is_topic_message = is_topic_message
# Create user mock
from_user = MagicMock()
from_user.id = user_id
from_user.username = username
message.from_user = from_user
# Set message content
message.text = message_text
message.media_group_id = media_group_id
message.photo = photo
message.video = video
message.document = document
message.voice = voice
message.sticker = sticker
message.reply_to_message = reply_to_message
message.caption = caption
message.entities = entities
message.caption_entities = caption_entities
update.message = message
update.effective_chat = message.chat
return update
def create_mock_file(file_path: str = "https://api.telegram.org/file/test.jpg"):
"""创建模拟的 Telegram File 对象。
Args:
file_path: 文件路径
Returns:
MagicMock: 模拟的 File 对象
"""
file = MagicMock()
file.file_path = file_path
file.get_file = AsyncMock(return_value=file)
return file
# ============================================================
# Discord 辅助函数
# ============================================================
def create_mock_discord_attachment(
filename: str = "test.txt",
url: str = "https://cdn.discordapp.com/test.txt",
content_type: str | None = None,
size: int = 1024,
):
"""创建模拟的 Discord Attachment 对象。
Args:
filename: 文件名
url: 文件 URL
content_type: 内容类型
size: 文件大小
Returns:
MagicMock: 模拟的 Attachment 对象
"""
attachment = MagicMock()
attachment.filename = filename
attachment.url = url
attachment.content_type = content_type
attachment.size = size
return attachment
def create_mock_discord_user(
user_id: int = 123456789,
name: str = "TestUser",
display_name: str = "Test User",
bot: bool = False,
):
"""创建模拟的 Discord User 对象。
Args:
user_id: 用户 ID
name: 用户名
display_name: 显示名
bot: 是否为机器人
Returns:
MagicMock: 模拟的 User 对象
"""
user = MagicMock()
user.id = user_id
user.name = name
user.display_name = display_name
user.bot = bot
user.mention = f"<@{user_id}>"
return user
def create_mock_discord_channel(
channel_id: int = 111222333,
channel_type: str = "text",
name: str = "general",
guild_id: int | None = 444555666,
):
"""创建模拟的 Discord Channel 对象。
Args:
channel_id: 频道 ID
channel_type: 频道类型
name: 频道名
guild_id: 服务器 ID
Returns:
MagicMock: 模拟的 Channel 对象
"""
channel = MagicMock()
channel.id = channel_id
channel.name = name
channel.type = channel_type
if guild_id:
channel.guild = MagicMock()
channel.guild.id = guild_id
else:
channel.guild = None
return channel
# ============================================================
# 消息组件辅助函数
# ============================================================
def create_mock_message_component(
component_type: str,
**kwargs: Any,
) -> BaseMessageComponent:
"""创建模拟的消息组件。
Args:
component_type: 组件类型 (plain, image, at, reply, file)
**kwargs: 组件参数
Returns:
BaseMessageComponent: 消息组件实例
"""
from astrbot.core.message import components as Comp
component_map = {
"plain": Comp.Plain,
"image": Comp.Image,
"at": Comp.At,
"reply": Comp.Reply,
"file": Comp.File,
}
component_class = component_map.get(component_type.lower())
if not component_class:
raise ValueError(f"Unknown component type: {component_type}")
return component_class(**kwargs)
def create_mock_llm_response(
completion_text: str = "Hello! How can I help you?",
role: str = "assistant",
tools_call_name: list[str] | None = None,
tools_call_args: list[dict] | None = None,
tools_call_ids: list[str] | None = None,
):
"""创建模拟的 LLM 响应。
Args:
completion_text: 完成文本
role: 角色
tools_call_name: 工具调用名称列表
tools_call_args: 工具调用参数列表
tools_call_ids: 工具调用 ID 列表
Returns:
LLMResponse: 模拟的 LLM 响应
"""
from astrbot.core.provider.entities import LLMResponse, TokenUsage
return LLMResponse(
role=role,
completion_text=completion_text,
tools_call_name=tools_call_name or [],
tools_call_args=tools_call_args or [],
tools_call_ids=tools_call_ids or [],
usage=TokenUsage(input_other=10, output=5),
)
+33
View File
@@ -0,0 +1,33 @@
{
"plain_message": {
"type": "plain",
"text": "Hello, this is a test message."
},
"image_message": {
"type": "image",
"url": "https://example.com/test.jpg",
"file": null
},
"at_message": {
"type": "at",
"user_id": "12345",
"nickname": "TestUser"
},
"reply_message": {
"type": "reply",
"id": "msg_123",
"sender_nickname": "OriginalSender",
"message_str": "This is the original message"
},
"file_message": {
"type": "file",
"name": "test.pdf",
"url": "https://example.com/test.pdf"
},
"combined_message": {
"components": [
{"type": "at", "user_id": "bot_id"},
{"type": "plain", "text": " Hello bot!"}
]
}
}
+43
View File
@@ -0,0 +1,43 @@
"""测试 Mock 模块。
提供统一的 mock 工具和 fixture,减少测试代码重复。
使用方式:
# 在测试文件顶部导入需要的 fixture
from tests.fixtures.mocks import mock_telegram_modules
# 或使用 Builder 类创建 mock 对象
from tests.fixtures.mocks import MockTelegramBuilder
bot = MockTelegramBuilder.create_bot()
"""
from .aiocqhttp import (
MockAiocqhttpBuilder,
create_mock_aiocqhttp_modules,
mock_aiocqhttp_modules,
)
from .discord import (
MockDiscordBuilder,
create_mock_discord_modules,
mock_discord_modules,
)
from .telegram import (
MockTelegramBuilder,
create_mock_telegram_modules,
mock_telegram_modules,
)
__all__ = [
# Telegram
"mock_telegram_modules",
"create_mock_telegram_modules",
"MockTelegramBuilder",
# Discord
"mock_discord_modules",
"create_mock_discord_modules",
"MockDiscordBuilder",
# Aiocqhttp
"mock_aiocqhttp_modules",
"create_mock_aiocqhttp_modules",
"MockAiocqhttpBuilder",
]
+58
View File
@@ -0,0 +1,58 @@
"""Aiocqhttp 模块 Mock 工具。
提供统一的 aiocqhttp 相关模块 mock 设置,避免在测试文件中重复定义。
"""
import sys
from unittest.mock import AsyncMock, MagicMock
import pytest
def create_mock_aiocqhttp_modules():
"""创建 aiocqhttp 相关的 mock 模块。
Returns:
dict: 包含 aiocqhttp 和相关模块的 mock 对象
"""
mock_aiocqhttp = MagicMock()
mock_aiocqhttp.CQHttp = MagicMock
mock_aiocqhttp.Event = MagicMock
mock_aiocqhttp.exceptions = MagicMock()
mock_aiocqhttp.exceptions.ActionFailed = Exception
return mock_aiocqhttp
@pytest.fixture(scope="module", autouse=True)
def mock_aiocqhttp_modules():
"""Mock aiocqhttp 相关模块的 fixture。
自动应用于使用此 fixture 的测试模块。
"""
mock_aiocqhttp = create_mock_aiocqhttp_modules()
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setitem(sys.modules, "aiocqhttp", mock_aiocqhttp)
monkeypatch.setitem(sys.modules, "aiocqhttp.exceptions", mock_aiocqhttp.exceptions)
yield
monkeypatch.undo()
class MockAiocqhttpBuilder:
"""构建 aiocqhttp 测试 mock 对象的工具类。"""
@staticmethod
def create_bot():
"""创建 mock CQHttp bot 实例。"""
from tests.fixtures.helpers import NoopAwaitable
bot = MagicMock()
bot.send = AsyncMock()
bot.call_action = AsyncMock()
bot.on_request = MagicMock()
bot.on_notice = MagicMock()
bot.on_message = MagicMock()
bot.on_websocket_connection = MagicMock()
bot.run_task = MagicMock(return_value=NoopAwaitable())
return bot
+140
View File
@@ -0,0 +1,140 @@
"""Discord 模块 Mock 工具。
提供统一的 Discord 相关模块 mock 设置,避免在测试文件中重复定义。
"""
import sys
from unittest.mock import AsyncMock, MagicMock
import pytest
def create_mock_discord_modules():
"""创建 Discord 相关的 mock 模块。
Returns:
dict: 包含 discord 和相关模块的 mock 对象
"""
mock_discord = MagicMock()
# Mock discord.Intents
mock_intents = MagicMock()
mock_intents.default = MagicMock(return_value=mock_intents)
mock_discord.Intents = mock_intents
# Mock discord.Status
mock_discord.Status = MagicMock()
mock_discord.Status.online = "online"
# Mock discord.Bot
mock_bot = MagicMock()
mock_discord.Bot = MagicMock(return_value=mock_bot)
# Mock discord.Embed
mock_embed = MagicMock()
mock_discord.Embed = MagicMock(return_value=mock_embed)
# Mock discord.ui
mock_ui = MagicMock()
mock_ui.View = MagicMock
mock_ui.Button = MagicMock
mock_discord.ui = mock_ui
# Mock discord.Message
mock_discord.Message = MagicMock
# Mock discord.Interaction
mock_discord.Interaction = MagicMock
mock_discord.InteractionType = MagicMock()
mock_discord.InteractionType.application_command = 2
mock_discord.InteractionType.component = 3
# Mock discord.File
mock_discord.File = MagicMock
# Mock discord.SlashCommand
mock_discord.SlashCommand = MagicMock
# Mock discord.Option
mock_discord.Option = MagicMock
# Mock discord.SlashCommandOptionType
mock_discord.SlashCommandOptionType = MagicMock()
mock_discord.SlashCommandOptionType.string = 3
# Mock discord.errors
mock_discord.errors = MagicMock()
mock_discord.errors.LoginFailure = Exception
mock_discord.errors.ConnectionClosed = Exception
mock_discord.errors.NotFound = Exception
mock_discord.errors.Forbidden = Exception
# Mock discord.abc
mock_discord.abc = MagicMock()
mock_discord.abc.GuildChannel = MagicMock
mock_discord.abc.Messageable = MagicMock
mock_discord.abc.PrivateChannel = MagicMock
# Mock discord.channel
mock_channel = MagicMock()
mock_channel.DMChannel = MagicMock
mock_discord.channel = mock_channel
# Mock discord.types
mock_discord.types = MagicMock()
mock_discord.types.interactions = MagicMock()
# Mock discord.ApplicationContext
mock_discord.ApplicationContext = MagicMock
# Mock discord.CustomActivity
mock_discord.CustomActivity = MagicMock
return mock_discord
@pytest.fixture(scope="module", autouse=True)
def mock_discord_modules():
"""Mock Discord 相关模块的 fixture。
自动应用于使用此 fixture 的测试模块。
"""
mock_discord = create_mock_discord_modules()
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setitem(sys.modules, "discord", mock_discord)
monkeypatch.setitem(sys.modules, "discord.abc", mock_discord.abc)
monkeypatch.setitem(sys.modules, "discord.channel", mock_discord.channel)
monkeypatch.setitem(sys.modules, "discord.errors", mock_discord.errors)
monkeypatch.setitem(sys.modules, "discord.types", mock_discord.types)
monkeypatch.setitem(
sys.modules,
"discord.types.interactions",
mock_discord.types.interactions,
)
monkeypatch.setitem(sys.modules, "discord.ui", mock_discord.ui)
yield
monkeypatch.undo()
class MockDiscordBuilder:
"""构建 Discord 测试 mock 对象的工具类。"""
@staticmethod
def create_client():
"""创建 mock Discord client 实例。"""
client = MagicMock()
client.user = MagicMock()
client.user.id = 123456789
client.user.display_name = "TestBot"
client.user.name = "TestBot"
client.get_channel = MagicMock()
client.fetch_channel = AsyncMock()
client.get_message = MagicMock()
client.start = AsyncMock()
client.close = AsyncMock()
client.is_closed = MagicMock(return_value=False)
client.add_application_command = MagicMock()
client.sync_commands = AsyncMock()
client.change_presence = AsyncMock()
return client
+141
View File
@@ -0,0 +1,141 @@
"""Telegram 模块 Mock 工具。
提供统一的 Telegram 相关模块 mock 设置,避免在测试文件中重复定义。
"""
import sys
from unittest.mock import AsyncMock, MagicMock
import pytest
def create_mock_telegram_modules():
"""创建 Telegram 相关的 mock 模块。
Returns:
dict: 包含 telegram 和相关模块的 mock 对象
"""
mock_telegram = MagicMock()
mock_telegram.BotCommand = MagicMock
mock_telegram.Update = MagicMock
mock_telegram.constants = MagicMock()
mock_telegram.constants.ChatType = MagicMock()
mock_telegram.constants.ChatType.PRIVATE = "private"
mock_telegram.constants.ChatAction = MagicMock()
mock_telegram.constants.ChatAction.TYPING = "typing"
mock_telegram.constants.ChatAction.UPLOAD_VOICE = "upload_voice"
mock_telegram.constants.ChatAction.UPLOAD_DOCUMENT = "upload_document"
mock_telegram.constants.ChatAction.UPLOAD_PHOTO = "upload_photo"
mock_telegram.error = MagicMock()
mock_telegram.error.BadRequest = Exception
mock_telegram.ReactionTypeCustomEmoji = MagicMock
mock_telegram.ReactionTypeEmoji = MagicMock
mock_telegram_ext = MagicMock()
mock_telegram_ext.ApplicationBuilder = MagicMock
mock_telegram_ext.ContextTypes = MagicMock
mock_telegram_ext.ExtBot = MagicMock
mock_telegram_ext.filters = MagicMock()
mock_telegram_ext.filters.ALL = MagicMock()
mock_telegram_ext.MessageHandler = MagicMock
# Mock telegramify_markdown
mock_telegramify = MagicMock()
mock_telegramify.markdownify = lambda text, **kwargs: text
# Mock apscheduler
mock_apscheduler = MagicMock()
mock_apscheduler.schedulers = MagicMock()
mock_apscheduler.schedulers.asyncio = MagicMock()
mock_apscheduler.schedulers.asyncio.AsyncIOScheduler = MagicMock
mock_apscheduler.schedulers.background = MagicMock()
mock_apscheduler.schedulers.background.BackgroundScheduler = MagicMock
return {
"telegram": mock_telegram,
"telegram.ext": mock_telegram_ext,
"telegramify_markdown": mock_telegramify,
"apscheduler": mock_apscheduler,
}
@pytest.fixture(scope="module", autouse=True)
def mock_telegram_modules():
"""Mock Telegram 相关模块的 fixture。
自动应用于使用此 fixture 的测试模块。
"""
mocks = create_mock_telegram_modules()
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setitem(sys.modules, "telegram", mocks["telegram"])
monkeypatch.setitem(sys.modules, "telegram.constants", mocks["telegram"].constants)
monkeypatch.setitem(sys.modules, "telegram.error", mocks["telegram"].error)
monkeypatch.setitem(sys.modules, "telegram.ext", mocks["telegram.ext"])
monkeypatch.setitem(sys.modules, "telegramify_markdown", mocks["telegramify_markdown"])
monkeypatch.setitem(sys.modules, "apscheduler", mocks["apscheduler"])
monkeypatch.setitem(
sys.modules, "apscheduler.schedulers", mocks["apscheduler"].schedulers
)
monkeypatch.setitem(
sys.modules,
"apscheduler.schedulers.asyncio",
mocks["apscheduler"].schedulers.asyncio,
)
monkeypatch.setitem(
sys.modules,
"apscheduler.schedulers.background",
mocks["apscheduler"].schedulers.background,
)
yield
monkeypatch.undo()
class MockTelegramBuilder:
"""构建 Telegram 测试 mock 对象的工具类。"""
@staticmethod
def create_bot():
"""创建 mock Telegram bot 实例。"""
bot = MagicMock()
bot.username = "test_bot"
bot.id = 12345678
bot.base_url = "https://api.telegram.org/bottest_token_123/"
bot.send_message = AsyncMock()
bot.send_photo = AsyncMock()
bot.send_document = AsyncMock()
bot.send_voice = AsyncMock()
bot.send_chat_action = AsyncMock()
bot.delete_my_commands = AsyncMock()
bot.set_my_commands = AsyncMock()
bot.set_message_reaction = AsyncMock()
bot.edit_message_text = AsyncMock()
return bot
@staticmethod
def create_application():
"""创建 mock Telegram Application 实例。"""
from tests.fixtures.helpers import NoopAwaitable
app = MagicMock()
app.bot = MagicMock()
app.bot.username = "test_bot"
app.bot.base_url = "https://api.telegram.org/bottest_token_123/"
app.initialize = AsyncMock()
app.start = AsyncMock()
app.stop = AsyncMock()
app.add_handler = MagicMock()
app.updater = MagicMock()
app.updater.start_polling = MagicMock(return_value=NoopAwaitable())
app.updater.stop = AsyncMock()
return app
@staticmethod
def create_scheduler():
"""创建 mock APScheduler 实例。"""
scheduler = MagicMock()
scheduler.add_job = MagicMock()
scheduler.start = MagicMock()
scheduler.running = True
scheduler.shutdown = MagicMock()
return scheduler
+40
View File
@@ -0,0 +1,40 @@
"""
测试插件 - 用于插件系统测试
这是一个最小化的测试插件,用于验证插件系统的功能。
"""
from astrbot.api import llm_tool, star
from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter
@star.register("test_plugin", "AstrBot Team", "测试插件 - 用于插件系统测试", "1.0.0")
class TestPlugin(star.Star):
"""测试插件类"""
def __init__(self, context: star.Context) -> None:
super().__init__(context)
self.initialized = True
async def terminate(self) -> None:
"""插件终止"""
self.initialized = False
@filter.command("test_cmd")
async def test_command(self, event: AstrMessageEvent) -> None:
"""测试命令处理器。"""
event.set_result(MessageEventResult().message("测试命令执行成功"))
@llm_tool("test_tool")
async def test_llm_tool(self, query: str) -> str:
"""测试 LLM 工具。
Args:
query(string): 查询内容。
"""
return f"测试工具执行成功: {query}"
@filter.regex(r"^test_regex_(.+)$")
async def test_regex_handler(self, event: AstrMessageEvent) -> None:
"""测试正则处理器。"""
event.set_result(MessageEventResult().message("正则匹配成功"))
+5
View File
@@ -0,0 +1,5 @@
name: test_plugin
description: 测试插件 - 用于插件系统测试
version: 1.0.0
author: AstrBot Team
repo: https://github.com/test/test_plugin