安装方式
手动下载安装
下载 ZIP 后解压到技能目录即可安装。若在桌面客户端 WebView中直接下载出现异常,本站会改为提示页 + 原始链接,请按页内说明操作。
下载 ZIP (shub-clean-pytest-v1.0.0.zip)触发指令
/clean-pytest
跨平台安装指引
该技能声明兼容以下 1 个平台,将 ZIP 解压到对应目录即可被识别。
unzip shub-clean-pytest-v1.0.0.zip -d ~/.claude/skills/
mkdir -p 创建;启用 Skill 后请重启对应 Agent 让配置生效。
使用指南
整洁 Pytest
围绕 整洁 Pytest:编写可维护的 Python 单测:fixture、参数化、目录组织与覆盖率;示例命令见 SKILL.md。 无需在每次任务前把零散英文说明手工拼进上下文,也 减少 与客户端默认行为脱节的试错;具体命令、钩子与 JSON 参数仍以 ZIP 包内 SKILL.md 为权威。下文结构与站内 MCP CLI 类专题稿相同:何时用、前置、流程、速查与故障。
何时使用
- 编写可维护的 Python 单测:fixture、参数化、目录组织与覆盖率
- 示例命令见 SKILL.md
- 已获取本技能 ZIP,并准备在 Claude Code / OpenClaw 中按 SKILL.md 挂载。
- 希望用中文专题稿快速判断「该不该启用」,再深入英文 SKILL 查参数与边界。
- 需要与团队对齐同一套触发方式、目录约定或回调格式时。
前置条件
- 通用:可运行 Claude Code 或文档要求的客户端;有可读写的项目工作区(或 SKILL.md 指定的沙箱目录)。
- 权威细节:API Key / OAuth、钩子路径、环境变量以 ZIP 内 SKILL.md 为准。
典型流程
- 从 ClawHub / 站内分发获取技能 ZIP,校验版本与校验和(若提供)。
- 阅读 SKILL.md 的安装段落:目录落点、客户端类型(Claude Code / OpenClaw / 脚本)。
- 用文档中的最小示例完成第一次调用(单文件修改、单次查询或单次委派)。
- 确认工作目录、权限边界与输出路径后,再处理多文件或长耗时任务。
- 需要回调 / Webhook / 通知时,按 SKILL.md 配置端点并在测试环境先验通。
与 ZIP / SKILL.md 的关系
站内专题稿与 MCP CLI 类 oss 稿同样:概括何时用、怎么接、怎么排错;命令模板、钩子名、JSON 字段、版本矩阵一律以 ZIP 内 SKILL.md 与 ClawHub 上游为准。
命令示例(摘自包内 SKILL.md)
以下为从上游 SKILL.md(或入库正文)自动抽取的终端/脚本片段;路径、环境变量与参数以当前 ZIP 与官方说明为准。
ClawHub slug:clean-pytest(安装命令以 SKILL.md / claw CLI 为准)。
# Run all tests
pytest
# Run with coverage
pytest --cov=myapp --cov-report=term-missing
# Run specific test file
pytest tests/test_user_service.py
# Run specific test
pytest tests/test_user_service.py::test_add_user_success
# Run parametrized tests with verbose output
pytest -v tests/test_user_service.py::test_add_user_parametrized
# Skip integration tests
pytest -m "not integration"
# Run only integration tests
pytest -m integration
# Stop on first failure
pytest -x
# Show local variables on failure
pytest -l
# Run tests in parallel (with pytest-xdist)
pytest -n auto
站内入库时的触发命令(完整语义见 ZIP):
# 使用本技能时可在对话中引用或执行上述指令;完整参数与示例见下载包内 SKILL.md。
/clean-pytest
最佳实践
- 先 SKILL.md 再猜参数;站内专题稿不替代 schema 与必填字段说明。
- 委派任务时写清验收标准(命令、文件路径、测试命令),减少来回追问。
- 长任务用文档推荐的回调 / 日志落盘代替高频轮询,省 Token 也省机器负载。
- 多技能同时启用时,注意钩子加载顺序与重复工具调用(以 SKILL.md 冲突说明为准)。
调试与排错
- 打开 stderr 与客户端日志;PTY/tmux 场景同时看面板最后几十行输出。
- 参数错误时对照 SKILL.md 中的 JSON/CLI 示例(引号、转义、工作目录)。
- 网络类失败:查代理、防火墙、MCP 传输方式(stdio / HTTP / SSE)。
速查
| 动作 | 说明 |
|------|------|
| 获取技能包 | ClawHub / 站内 ZIP,核对版本 |
| 权威步骤 | 优先阅读 ZIP 内 SKILL.md |
| 首次试跑 | 使用 SKILL.md 最小示例 |
| 验收 | 对照路径、测试命令或回调负载 |
常见故障
- 无输出或立即退出 → 工作目录错误、依赖未装、或 Claude Code 未登录;按 SKILL.md 自检清单执行。
- 权限被拒绝 → 检查沙箱路径、
--permission-mode与工具白名单。 - 与简介不符 → 以英文 SKILL 与上游仓库为准,站内稿仅作结构化导读。
# Clean Pytest
Clean, maintainable pytest test patterns using Fake-based testing, contract testing, and dependency injection. Focuses on test isolation, reusability, and clarity through explicit AAA pattern and well-structured fixtures.
## When to Use
- Setting up test suites for Python/MCP projects
- Creating Fake implementations for external dependencies
- Writing contract tests for MCP tools/controllers
- Implementing test patterns with dependency injection
- Testing layered architectures (Controllers → Services → Repositories)
- Writing parametrized tests for multiple scenarios
## Core Principles
### 1. Fakes over Mocks
Use **Fake classes** instead of mocking with `unittest.mock`. Fakes are in-memory implementations that mimic real dependencies without external calls.
**Why Fakes?**
- More readable and maintainable
- Easier to debug
- Better test isolation
- No monkey-patching magic
- Self-documenting behavior
### 2. Explicit AAA Pattern
Structure every test into three clear phases with comments:
```python
# Arrange
# Set up test data and dependencies
# Act
# Execute the code under test
# Assert
# Verify the result
```
### 3. Dependency Injection in Fixtures
Inject dependencies between fixtures to maintain relationships and avoid duplication.
### 4. Contract Testing
Verify that components register tools/functions correctly and pass expected arguments.
## Architecture Pattern
```
Controller (MCP Tools)
↓
Service (Business Logic)
↓
Repository (Data Access)
↓
Fake (Test Implementation)
```
## Creating Fakes
### Basic Fake Structure
Create a Fake class that implements the same interface as the real dependency:
```python
# tests/fakes.py
from typing import Any, Dict, List, Optional
class FakeAuth:
"""Fake implementation of AuthProvider for testing."""
def __init__(self) -> None:
self.created: List[Dict[str, Any]] = []
self.deleted: List[str] = []
self._seq = 0
self.fail_on_create: bool = False
def create_user(self, email: str, password: str, display_name: str) -> str:
if self.fail_on_create:
raise RuntimeError("create_user failed (fake)")
self._seq += 1
uid = f"uid-{self._seq}"
rec = {"uid": uid, "email": email, "display_name": display_name}
self.created.append(rec)
return uid
def delete_user(self, uid: str) -> None:
self.deleted.append(uid)
```
### Repository Fake
```python
class FakeUsersRepo:
"""Fake implementation of UsersRepository."""
def __init__(self) -> None:
self.users: Dict[str, Dict[str, Any]] = {}
self.fail_on_upsert: bool = False
def upsert_user_doc(self, uid: str, data: Dict[str, Any]) -> None:
if self.fail_on_upsert:
raise RuntimeError("upsert_user_doc failed (fake)")
self.users[uid] = dict(data)
def list_users(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
items = list(self.users.values())
if limit and limit > 0:
items = items[:limit]
return [dict(it) for it in items]
```
### Controlled Failure Fakes
```python
class FakeAuth:
def __init__(self) -> None:
self.fail_on_create: bool = False # Control failure in tests
def create_user(self, email: str, password: str, display_name: str) -> str:
if self.fail_on_create:
raise RuntimeError("create_user failed (fake)")
# ... rest of implementation
```
### Nested Repository Fakes
```python
class FakeSectorsRepo:
def __init__(self, institutions: FakeInstitutionsRepo | None = None) -> None:
self.institutions = institutions # Inject dependency
self.data: Dict[str, Dict[str, Dict[str, Any]]] = {}
def institution_exists(self, institution_id: str) -> bool:
return bool(self.institutions and institution_id in self.institutions.data)
def upsert_sector(self, institution_id: str, sector_id: str, data: Dict[str, Any]) -> None:
self.data.setdefault(institution_id, {})[sector_id] = dict(data)
```
## Fixtures
### Basic Fixture (conftest.py)
```python
# tests/conftest.py
import pytest
from tests.fakes import FakeAuth, FakeUsersRepo
@pytest.fixture()
def fake_auth():
"""Provide a fresh FakeAuth for each test."""
return FakeAuth()
@pytest.fixture()
def fake_users_repo():
"""Provide a fresh FakeUsersRepo for each test."""
return FakeUsersRepo()
```
### Fixture with Dependency Injection
```python
@pytest.fixture()
def fake_sectors_repo(fake_institutions_repo):
"""FakeSectorsRepo depends on FakeInstitutionsRepo."""
return FakeSectorsRepo(institutions=fake_institutions_repo)
@pytest.fixture()
def fake_rooms_repo(fake_sectors_repo):
"""FakeRoomsRepo depends on FakeSectorsRepo."""
return FakeRoomsRepo(sectors=fake_sectors_repo)
```
### Environment Fixture
```python
@pytest.fixture()
def user_env(fake_auth, fake_users_repo):
"""Provide service and all dependencies for user operations."""
from myapp.services.user_service import UserService
svc = UserService(fake_auth, fake_users_repo)
return svc, fake_auth, fake_users_repo
```
### Seeded Environment Fixture
```python
@pytest.fixture()
def user_env_seeded(user_env):
"""Environment with pre-seeded data."""
svc, auth, repo = user_env
svc.add_user(email="test@example.com", password="secret", name="Test User")
return svc
```
### Fixture with Cleanup
```python
@pytest.fixture()
def temp_file():
"""Provide a temporary file and clean up after test."""
import tempfile
import os
fd, path = tempfile.mkstemp()
os.close(fd)
yield path
os.unlink(path)
```
## Service Layer Testing
### Basic AAA Pattern Test
```python
# tests/test_user_service.py
import pytest
from myapp.services.user_service import UserService
def test_add_user_success(fake_auth, fake_users_repo):
# Arrange
svc = UserService(fake_auth, fake_users_repo)
email = "test@example.com"
password = "secret"
name = "Test User"
# Act
result = svc.add_user(email=email, password=password, name=name)
# Assert
assert result["status"] == "ok"
assert result["user"]["email"] == email
assert result["user"]["name"] == name
assert result["uid"] in fake_users_repo.users
```
### Parametrized Tests
```python
@pytest.mark.parametrize(
"email,password,name,role",
[
("a@example.com", "secret", "Alice", "admin"),
("b@example.com", "p@ss", "Bob", "user"),
],
)
def test_add_user_parametrized(user_env, email, password, name, role):
svc, _auth, _repo = user_env
# Act
res = svc.add_user(email=email, password=password, name=name, global_role=role)
# Assert
assert res["status"] == "ok"
assert res["user"]["email"] == email
assert res["user"]["name"] == name
assert res["user"]["globalRole"] == role
```
### Testing Error Scenarios with Fakes
```python
@pytest.mark.parametrize("email", ["c@example.com", "d@example.com"])
def test_add_user_rollback_on_firestore_failure(fake_auth, fake_users_repo, email):
# Arrange
fake_users_repo.fail_on_upsert = True
svc = UserService(fake_auth, fake_users_repo)
# Act & Assert
with pytest.raises(RuntimeError):
svc.add_user(email=email, password="secret", name="Bob")
# Assert rollback
assert fake_auth.deleted, "Expected auth user to be deleted on Firestore failure"
```
### Testing Timestamp Normalization
```python
def test_list_users_normalizes_timestamps_to_iso(user_env):
# Arrange
svc, _auth, repo = user_env
from datetime import datetime
repo.users["u1"] = {
"id": "u1",
"email": "x@y.z",
"name": "X",
"globalRole": "user",
"createdAt": datetime(2024, 1, 1),
"updatedAt": datetime(2024, 1, 2),
}
# Act
res = svc.list_users(limit=10)
# Assert
assert res["status"] == "ok"
assert res["count"] == 1
user = res["users"][0]
assert isinstance(user["createdAt"], str)
assert isinstance(user["updatedAt"], str)
```
## Contract Testing
### MCP Tool Registration Contract
Test that controllers properly register tools with expected signatures:
```python
# tests/test_controllers_contract.py
from typing import Any, Callable, Dict
class FakeMCP:
"""Minimal FakeMCP for contract testing."""
def __init__(self) -> None:
self.tools: Dict[str, Callable[..., Any]] = {}
self.meta: Dict[str, Dict[str, Any]] = {}
def tool(self, name: str, description: str, tags: Optional[set] = None, meta: Optional[dict] = None):
def decorator(fn: Callable[..., Any]):
self.tools[name] = fn
self.meta[name] = {
"description": description,
"tags": set(tags or set()),
"meta": dict(meta or {}),
}
return fn
return decorator
class FakeUserService:
"""Simple fake service that records calls."""
def __init__(self):
self.calls = []
def add_user(self, **kwargs):
self.calls.append(("add_user", kwargs))
return {"status": "ok", "op": "add_user", "args": kwargs}
def test_users_controller_contract():
# Arrange
from myapp.controllers.users_controller import UsersController
fake = FakeMCP()
svc = FakeUserService()
UsersController(fake, svc)
# Assert tool registration
assert "add_user" in fake.tools
assert "list_users" in fake.tools
# Act & Assert tool behavior
res = fake.tools["add_user"](
email="a@x.y", password="s3cr3t", name="Alice", global_role="admin"
)
assert res["status"] == "ok"
assert res["op"] == "add_user"
assert res["args"]["email"] == "a@x.y"
```
### Parametrized Contract Tests
```python
@pytest.mark.parametrize(
"email,password,name,role",
[
("a@x.y", "s3cr3t", "Alice", "admin"),
("b@x.y", "p@ssw0rd", "Bob", "user"),
],
)
def test_users_add_user_parametrized(_users_env, email, password, name, role):
# Arrange
fake, _ = _users_env
# Act
res = fake.tools["add_user"](
email=email, password=password, name=name, global_role=role
)
# Assert
assert res["status"] == "ok"
assert res["op"] == "add_user"
assert res["args"]["email"] == email
```
## Repository Layer Testing
### Testing Repository Operations
```python
@pytest.fixture()
def repo_env(fake_institutions_repo, fake_sectors_repo):
# Seed data
fake_institutions_repo.upsert("inst1", {"id": "inst1", "name": "Inst One"})
fake_sectors_repo.upsert_sector(
"inst1", "er", {"id": "er", "name": "ER", "slug": "er", "isActive": True}
)
return fake_sectors_repo
```
### Testing Multiple Data Scenarios
```python
@pytest.mark.parametrize("rooms", [
["101"],
["201", {"name": "102", "id": "room-102"}],
])
def test_add_and_list_rooms(room_env, rooms):
svc, _ = room_env
# Act
res = svc.add_sector_rooms("inst1", "er", rooms)
# Assert
assert res["status"] == "ok"
assert res["count"] == len(rooms)
lst = svc.list_sector_rooms("inst1", "er", limit=10)
assert lst["status"] == "ok"
assert lst["count"] == len(rooms)
```
### Testing Limit Behavior
```python
@pytest.mark.parametrize("limit", [1, 3])
def test_list_rooms_limits(room_env_seeded, limit):
svc = room_env_seeded
# Act
lst = svc.list_sector_rooms("inst1", "er", limit=limit)
# Assert
assert lst["status"] == "ok"
assert lst["count"] == min(2, limit) # 2 items seeded
```
### Testing Not Found Scenarios
```python
@pytest.mark.parametrize("room_id,deleted", [
("room-102", True),
("room-999", False),
])
def test_remove_rooms_parametrized(room_env_seeded, room_id, deleted):
svc = room_env_seeded
# Act
res = svc.remove_sector_room("inst1", "er", room_id)
# Assert
assert res["deleted"] is deleted
if not deleted:
assert res.get("reason") == "room_not_found"
```
## Integration Testing
### Conditional Integration Tests
Skip integration tests when external dependencies are not available:
```python
# tests/test_integration_wiring.py
import os
import pytest
# Gate this integration test on presence of credentials
_ENV_KEYS = (
"FIREBASE_SERVICE_ACCOUNT",
"GOOGLE_APPLICATION_CREDENTIALS",
)
_has_env_creds = any(os.getenv(k) for k in _ENV_KEYS)
pytestmark = [
pytest.mark.integration,
pytest.mark.skipif(
not _has_env_creds,
reason=(
"Integration test requires Firebase Admin credentials via env "
"(FIREBASE_SERVICE_ACCOUNT or GOOGLE_APPLICATION_CREDENTIALS)"
),
),
]
@pytest.mark.integration
def test_build_app_initializes_and_registers_tools():
# Arrange
from myapp.wiring import build_app
# Act
app = build_app()
# Assert
assert hasattr(app, "run")
```
### Test Isolation
Each test should be independent and not share state:
```python
def test_user_created_in_one_test_not_visible_in_another(fake_auth, fake_users_repo):
# Arrange
svc1 = UserService(fake_auth, fake_users_repo)
# Act
result1 = svc1.add_user(email="test1@example.com", password="secret", name="User1")
# Assert - second test with fresh fixtures should not see this user
svc2 = UserService(fake_auth, fake_users_repo)
users = svc2.list_users()
assert users["count"] == 1 # Only the user from this test
```
## Testing Anti-Patterns to Avoid
### Don't Mock What You Don't Own
❌ Bad - Mocking external library:
```python
@patch('firebase_admin.auth.create_user')
def test_add_user(mock_create_user):
mock_create_user.return_value = Mock(uid="uid-1")
# ... test code
```
✅ Good - Use Fake for your interface:
```python
def test_add_user(fake_auth, fake_users_repo):
svc = UserService(fake_auth, fake_users_repo)
# ... test code
```
### Don't Test Implementation Details
❌ Bad - Testing internal method calls:
```python
def test_add_user(fake_auth, fake_users_repo):
svc = UserService(fake_auth, fake_users_repo)
svc.add_user(email="test@example.com", password="secret", name="User")
assert fake_auth.created == [{"uid": "uid-1", ...}] # Implementation detail
```
✅ Good - Testing observable behavior:
```python
def test_add_user(fake_auth, fake_users_repo):
svc = UserService(fake_auth, fake_users_repo)
result = svc.add_user(email="test@example.com", password="secret", name="User")
assert result["status"] == "ok"
assert result["user"]["email"] == "test@example.com"
```
### Don't Skip Error Paths
❌ Bad - Only happy path:
```python
def test_add_user_success(fake_auth, fake_users_repo):
# Only tests success case
```
✅ Good - Test all scenarios:
```python
def test_add_user_success(fake_auth, fake_users_repo):
# Happy path
def test_add_user_rollback_on_firestore_failure(fake_auth, fake_users_repo):
# Error path
def test_add_user_handles_duplicate_email(fake_auth, fake_users_repo):
# Edge case
```
## Running Tests
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=myapp --cov-report=term-missing
# Run specific test file
pytest tests/test_user_service.py
# Run specific test
pytest tests/test_user_service.py::test_add_user_success
# Run parametrized tests with verbose output
pytest -v tests/test_user_service.py::test_add_user_parametrized
# Skip integration tests
pytest -m "not integration"
# Run only integration tests
pytest -m integration
# Stop on first failure
pytest -x
# Show local variables on failure
pytest -l
# Run tests in parallel (with pytest-xdist)
pytest -n auto
```
## Best Practices Checklist
- [ ] Use Fake classes instead of `unittest.mock`
- [ ] Structure tests with explicit AAA comments
- [ ] Use fixtures for test setup
- [ ] Inject dependencies between fixtures
- [ ] Parametrize tests for multiple scenarios
- [ ] Test happy paths and error paths
- [ ] Test edge cases and boundaries
- [ ] Write contract tests for interfaces
- [ ] Ensure test isolation
- [ ] Use descriptive test names
- [ ] Keep tests focused on one behavior
- [ ] Avoid testing implementation details
- [ ] Test at appropriate level (unit vs integration)
- [ ] Mock external dependencies appropriately
- [ ] Maintain test coverage