关于 testing-python
面向 Python 项目的测试技能,聚焦 pytest、CLI 与端到端测试、容器化测试、模拟服务器和 pytest-qt。内容强调通过真实文件系统、HTTP 服务及可控二进制程序提升测试可信度。
主要功能
- 01pytest fixture 与测试目录组织
- 02CLI 和端到端测试模式
- 03临时目录与环境隔离
- 04pytest-httpserver 模拟 API
- 05容器化测试服务与模拟二进制
- 06异步测试及 pytest-qt
适用场景
- 01验证 Python 命令行应用的输出和退出码
- 02使用临时目录测试文件操作
- 03通过真实 HTTP 服务测试 API 交互
- 04为依赖服务的测试搭建容器环境
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ quick-brown-foxxx/ coding_rules_python --skill testing-python安装后在 Claude Code 中输入/testing-python确认已加载。
详细介绍
Python 测试
前提条件
此技能扩展了 myai 的 high-level-testing-strategy、test-driven-development 和 manual-testing。请先加载这些技能。using-my-skills 和 engineering-principles 假定已通过 myai 引导程序加载。
关于通用测试理念(可信度优先于覆盖率、端到端测试优先于单元测试、真实环境优先于模拟、帕累托原则),请参阅 myai 的 engineering-principles 和 high-level-testing-strategy。关于 TDD 的红-绿-重构工作流,请参阅 myai 的 test-driven-development。关于手动验证模式,请参阅 myai 的 manual-testing。此技能仅涵盖 Python 专属的测试工具和模式:pytest 固件、CLI/端到端测试模式、容器化测试、模拟服务和 pytest-qt。
测试规划与优先级
Python 专属测试优先级:
- CLI / 端到端测试 — 通过
subprocess.run(["uv", "run", "poe", "app", ...])运行实际命令,检查输出和退出码 - 集成测试 — 通过公共 API 测试组件交互,使用真实的临时目录和 pytest-httpserver
- 单元测试 — 纯数据转换函数
- 跳过 — 框架粘合代码、UI 布局、简单的 getter
目录结构
tests/
├── unit/ # Pure function tests
├── integration/ # CLI tests, component interaction
├── fixtures/ # Shared test data and helpers
└── conftest.py # Shared fixtures
pyproject.toml 配置
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
asyncio_mode = "auto"
addopts = ["-n", "auto", "--dist", "worksteal"]
markers = [
"unit: unit tests",
"integration: integration tests",
]
依赖项
[dependency-groups]
dev = [
"pytest>=9.0.1",
"pytest-xdist>=3.5.0",
"pytest-cov>=7.0.0",
"pytest-asyncio>=1.3.0",
# "pytest-qt>=4.5.0", # For Qt apps
# "pytest-httpserver>=1.1.0", # For HTTP mocking
]
测试示例
CLI 测试示例
import subprocess
def test_list_profiles_empty() -> None:
result = subprocess.run(
["uv", "run", "poe", "app", "list"],
capture_output=True, text=True,
)
assert result.returncode == 0
assert "No profiles found" in result.stdout
def test_create_and_list_profile(tmp_path: Path) -> None:
env = {**os.environ, "APP_DATA_DIR": str(tmp_path)}
subprocess.run(
["uv", "run", "poe", "app", "create", "test-profile"],
env=env, check=True,
)
result = subprocess.run(
["uv", "run", "poe", "app", "list"],
capture_output=True, text=True, env=env,
)
assert "test-profile" in result.stdout
Result 模式测试示例
def test_load_config_missing_file() -> None:
result = load_config(Path("/nonexistent"))
assert result.is_err
assert "not found" in result.unwrap_err()
def test_load_config_valid() -> None:
result = load_config(Path("tests/fixtures/valid_config.yaml"))
assert result.is_ok
config = result.unwrap()
assert config.name == "test"
异步测试示例
@pytest.mark.asyncio
async def test_fetch_data() -> None:
result = await fetch_data("https://httpbin.org/get")
assert result.is_ok
固件
临时目录
@pytest.fixture
def app_data_dir(tmp_path: Path) -> Path:
data_dir = tmp_path / "data"
data_dir.mkdir()
return data_dir
环境变量覆盖(Linux/XDG 示例)
@pytest.fixture
def isolated_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[Path, Path]:
config = tmp_path / "config"
data = tmp_path / "data"
config.mkdir()
data.mkdir()
monkeypatch.setenv("XDG_CONFIG_HOME", str(config))
monkeypatch.setenv("XDG_DATA_HOME", str(data))
return config, data
跨平台替代方案:将目录注入被测代码,或对 platformdirs.user_*_dir 调用进行 monkeypatch,而不是使用 XDG 环境变量。
示例数据
@pytest.fixture
def sample_audio_16khz() -> np.ndarray:
return np.zeros(16000, dtype=np.float32) # 1 second of silence
HTTP 服务模拟(真实服务,而非打补丁)
@pytest.fixture
def mock_api(httpserver: HTTPServer) -> HTTPServer:
httpserver.expect_request("/api/data").respond_with_json({"status": "ok"})
return httpserver
def test_fetch_from_api(mock_api: HTTPServer) -> None:
result = fetch_data(mock_api.url_for("/api/data"))
assert result.is_ok
运行测试
默认并行运行测试(通过 addopts 设置 -n auto)。可使用 -n0(顺序运行)或 -n4(指定数量)覆盖此设置。
uv run poe test # All tests (parallel, auto workers)
uv run pytest tests/unit/ # Unit only
uv run pytest -n0 # Force sequential (debugging)
uv run pytest --cov # With coverage report
测试隔离
Python 专属做法:使用 tmp_path 处理文件,使用 monkeypatch 处理环境变量,使用 yield 固件执行清理。
不稳定测试
关于不稳定测试的通用处理方法,请参阅 myai 的
systematic-debugging。
覆盖率指南
关于通用覆盖率理念(指导原则,而非目标),请参阅 myai 的
engineering-principles。 Python 专属的合理性检查:
| 范围 | 指导值 |
|---|---|
| 核心业务逻辑 | >70% |
| CLI 命令 | >70% |
| UI 组件 | >40% |
| 工具函数 | 按需 |
测试验证
关于通用测试质量审查流程,请参阅 myai 的
doing-code-review和verification-before-completion。
重型测试
当轻量级测试不够用时。理念相同,但基础设施复杂度更高。
平台说明: 下文的 DBus、docker/容器和 POSIX 模拟二进制文件模式仅适用于 Linux/主机环境。请通过 sys.platform / pytest.mark.skipif 控制这些测试,确保测试套件在 Windows 上也能通过。对于跨平台路径隔离,优先使用 tmp_path 并对 platformdirs 进行 monkeypatch。
核心理念
| 不要使用…… | 改用…… |
|---|---|
@patch("requests.get") | 真实的 HTTP 服务(pytest-httpserver 或自定义服务) |
@patch("subprocess.run") | 模拟真实程序行为的自定义轻量级二进制文件 |
用于数据库的 unittest.mock.Mock() | 容器中的真实数据库 |
| 经过 monkeypatch 的文件操作 | tmp_path 或容器卷中的真实文件系统 |
| 模拟的系统服务(DBus) | 用于测试的真实守护进程实例 |
容器化测试环境
tests/
├── containers/
│ ├── Dockerfile.test-env # Base test environment
│ ├── Dockerfile.mock-api # Mock API server
│ ├── docker-compose.test.yml # Orchestration
│ └── mock-bins/ # Custom mock binaries
│ ├── mock-telegram # Fake Telegram Desktop
│ └── mock-ffmpeg # Fake ffmpeg (returns predefined output)
├── integration/
│ └── test_with_containers.py
└── conftest.py # Container lifecycle fixtures
用于测试服务的 Docker Compose
# tests/containers/docker-compose.test.yml
services:
mock-api:
build:
context: .
dockerfile: Dockerfile.mock-api
ports:
- "18080:8080"
test-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: test
POSTGRES_PASSWORD: test
ports:
- "15432:5432"
容器生命周期固件
@pytest.fixture(scope="session")
def test_services():
"""Start all test containers, yield, then tear down."""
compose_file = Path(__file__).parent / "containers" / "docker-compose.test.yml"
subprocess.run(
["docker-compose", "-f", str(compose_file), "up", "-d", "--wait"],
check=True,
)
yield
subprocess.run(
["docker-compose", "-f", str(compose_file), "down", "-v"],
check=True,
)
模拟二进制文件
不要对 subprocess.run() 打补丁,而是提供行为可预测的真实二进制文件:
#!/bin/env python3
# tests/containers/mock-bins/mock-telegram
import sys, time, os
print("Telegram Desktop Mock v1.0")
print(f"Working directory: {os.getcwd()}")
if "-many" in sys.argv and "-workdir" in sys.argv:
print(f"Mock Telegram started in {sys.argv[sys.argv.index('-workdir') + 1]}")
time.sleep(int(os.environ.get("MOCK_TELEGRAM_LIFETIME", "5")))
sys.exit(0)
print("Unknown arguments", sys.argv, file=sys.stderr)
sys.exit(1)
@pytest.fixture
def mock_telegram_bin(tmp_path: Path) -> Path:
mock_bin = tmp_path / "telegram"
mock_bin.write_text(MOCK_TELEGRAM_SCRIPT)
mock_bin.chmod(0o755)
return mock_bin
async def test_start_instance(mock_telegram_bin: Path) -> None:
result = await start_instance(profile, binary_path=mock_telegram_bin)
assert result.is_ok
pid = result.unwrap()
assert pid > 0
有状态模拟 API
适用于需要在多个请求之间保持状态的 API:
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading, json
class MockAPIHandler(BaseHTTPRequestHandler):
profiles: dict[str, dict[str, str | int | bool]] = {}
def do_POST(self) -> None:
if self.path == "/api/profiles":
data = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
self.profiles[data["id"]] = data
self.send_response(201)
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def do_GET(self) -> None:
if self.path == "/api/profiles":
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(list(self.profiles.values())).encode())
@pytest.fixture(scope="session")
def mock_api() -> Generator[str, None, None]:
server = HTTPServer(("127.0.0.1", 0), MockAPIHandler)
port = server.server_address[1]
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
yield f"http://127.0.0.1:{port}"
server.shutdown()
真实服务测试(DBus)
@pytest.fixture(scope="session")
def dbus_session() -> Generator[str, None, None]:
"""Start a real DBus session daemon for tests."""
process = subprocess.Popen(
["dbus-daemon", "--session", "--print-address", "--nofork"],
stdout=subprocess.PIPE,
)
address = process.stdout.readline().decode().strip()
os.environ["DBUS_SESSION_BUS_ADDRESS"] = address
yield address
process.terminate()
process.wait()
相关 myai 技能
high-level-testing-strategy— 上级技能。BDD 优先的测试规划、行为场景、自动化范围和模拟边界。test-driven-development— 上级技能。自动化测试的红—绿—重构工作流。manual-testing— 上级技能。通过环境预检、隔离和证据进行贴近实际的手动验证。architecting-test-infra— 用于搭建测试基础设施、固定装置、状态隔离和种子层。engineering-principles— 与语言无关的测试理念:可信度优先于覆盖率,真实优于模拟。writing-python-code— 适用于测试代码的 Python 专属编码规则。