← 返回 Skills 市场
marcoracer

Clean Pytest

作者 Marco Borges · GitHub ↗ · v0.1.0
cross-platform ✓ 安全检测通过
826
总下载
0
收藏
5
当前安装
1
版本数
在 OpenClaw 中安装
/install clean-pytest
功能描述
Write clean, maintainable pytest tests using Fake-based testing, contract testing, and dependency injection patterns. Use when setting up test suites for Pyt...
使用说明 (SKILL.md)

\r \r

Clean Pytest\r

\r 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.\r \r

When to Use\r

\r

  • Setting up test suites for Python/MCP projects\r
  • Creating Fake implementations for external dependencies\r
  • Writing contract tests for MCP tools/controllers\r
  • Implementing test patterns with dependency injection\r
  • Testing layered architectures (Controllers → Services → Repositories)\r
  • Writing parametrized tests for multiple scenarios\r \r

Core Principles\r

\r

1. Fakes over Mocks\r

\r Use Fake classes instead of mocking with unittest.mock. Fakes are in-memory implementations that mimic real dependencies without external calls.\r \r Why Fakes?\r

  • More readable and maintainable\r
  • Easier to debug\r
  • Better test isolation\r
  • No monkey-patching magic\r
  • Self-documenting behavior\r \r

2. Explicit AAA Pattern\r

\r Structure every test into three clear phases with comments:\r \r

# Arrange\r
# Set up test data and dependencies\r
\r
# Act\r
# Execute the code under test\r
\r
# Assert\r
# Verify the result\r
```\r
\r
### 3. Dependency Injection in Fixtures\r
\r
Inject dependencies between fixtures to maintain relationships and avoid duplication.\r
\r
### 4. Contract Testing\r
\r
Verify that components register tools/functions correctly and pass expected arguments.\r
\r
## Architecture Pattern\r
\r
```\r
Controller (MCP Tools)\r
    ↓\r
Service (Business Logic)\r
    ↓\r
Repository (Data Access)\r
    ↓\r
Fake (Test Implementation)\r
```\r
\r
## Creating Fakes\r
\r
### Basic Fake Structure\r
\r
Create a Fake class that implements the same interface as the real dependency:\r
\r
```python\r
# tests/fakes.py\r
from typing import Any, Dict, List, Optional\r
\r
class FakeAuth:\r
    """Fake implementation of AuthProvider for testing."""\r
    def __init__(self) -> None:\r
        self.created: List[Dict[str, Any]] = []\r
        self.deleted: List[str] = []\r
        self._seq = 0\r
        self.fail_on_create: bool = False\r
\r
    def create_user(self, email: str, password: str, display_name: str) -> str:\r
        if self.fail_on_create:\r
            raise RuntimeError("create_user failed (fake)")\r
        self._seq += 1\r
        uid = f"uid-{self._seq}"\r
        rec = {"uid": uid, "email": email, "display_name": display_name}\r
        self.created.append(rec)\r
        return uid\r
\r
    def delete_user(self, uid: str) -> None:\r
        self.deleted.append(uid)\r
```\r
\r
### Repository Fake\r
\r
```python\r
class FakeUsersRepo:\r
    """Fake implementation of UsersRepository."""\r
    def __init__(self) -> None:\r
        self.users: Dict[str, Dict[str, Any]] = {}\r
        self.fail_on_upsert: bool = False\r
\r
    def upsert_user_doc(self, uid: str, data: Dict[str, Any]) -> None:\r
        if self.fail_on_upsert:\r
            raise RuntimeError("upsert_user_doc failed (fake)")\r
        self.users[uid] = dict(data)\r
\r
    def list_users(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:\r
        items = list(self.users.values())\r
        if limit and limit > 0:\r
            items = items[:limit]\r
        return [dict(it) for it in items]\r
```\r
\r
### Controlled Failure Fakes\r
\r
```python\r
class FakeAuth:\r
    def __init__(self) -> None:\r
        self.fail_on_create: bool = False  # Control failure in tests\r
\r
    def create_user(self, email: str, password: str, display_name: str) -> str:\r
        if self.fail_on_create:\r
            raise RuntimeError("create_user failed (fake)")\r
        # ... rest of implementation\r
```\r
\r
### Nested Repository Fakes\r
\r
```python\r
class FakeSectorsRepo:\r
    def __init__(self, institutions: FakeInstitutionsRepo | None = None) -> None:\r
        self.institutions = institutions  # Inject dependency\r
        self.data: Dict[str, Dict[str, Dict[str, Any]]] = {}\r
\r
    def institution_exists(self, institution_id: str) -> bool:\r
        return bool(self.institutions and institution_id in self.institutions.data)\r
\r
    def upsert_sector(self, institution_id: str, sector_id: str, data: Dict[str, Any]) -> None:\r
        self.data.setdefault(institution_id, {})[sector_id] = dict(data)\r
```\r
\r
## Fixtures\r
\r
### Basic Fixture (conftest.py)\r
\r
```python\r
# tests/conftest.py\r
import pytest\r
from tests.fakes import FakeAuth, FakeUsersRepo\r
\r
@pytest.fixture()\r
def fake_auth():\r
    """Provide a fresh FakeAuth for each test."""\r
    return FakeAuth()\r
\r
@pytest.fixture()\r
def fake_users_repo():\r
    """Provide a fresh FakeUsersRepo for each test."""\r
    return FakeUsersRepo()\r
```\r
\r
### Fixture with Dependency Injection\r
\r
```python\r
@pytest.fixture()\r
def fake_sectors_repo(fake_institutions_repo):\r
    """FakeSectorsRepo depends on FakeInstitutionsRepo."""\r
    return FakeSectorsRepo(institutions=fake_institutions_repo)\r
\r
@pytest.fixture()\r
def fake_rooms_repo(fake_sectors_repo):\r
    """FakeRoomsRepo depends on FakeSectorsRepo."""\r
    return FakeRoomsRepo(sectors=fake_sectors_repo)\r
```\r
\r
### Environment Fixture\r
\r
```python\r
@pytest.fixture()\r
def user_env(fake_auth, fake_users_repo):\r
    """Provide service and all dependencies for user operations."""\r
    from myapp.services.user_service import UserService\r
    svc = UserService(fake_auth, fake_users_repo)\r
    return svc, fake_auth, fake_users_repo\r
```\r
\r
### Seeded Environment Fixture\r
\r
```python\r
@pytest.fixture()\r
def user_env_seeded(user_env):\r
    """Environment with pre-seeded data."""\r
    svc, auth, repo = user_env\r
    svc.add_user(email="[email protected]", password="secret", name="Test User")\r
    return svc\r
```\r
\r
### Fixture with Cleanup\r
\r
```python\r
@pytest.fixture()\r
def temp_file():\r
    """Provide a temporary file and clean up after test."""\r
    import tempfile\r
    import os\r
    fd, path = tempfile.mkstemp()\r
    os.close(fd)\r
    yield path\r
    os.unlink(path)\r
```\r
\r
## Service Layer Testing\r
\r
### Basic AAA Pattern Test\r
\r
```python\r
# tests/test_user_service.py\r
import pytest\r
from myapp.services.user_service import UserService\r
\r
def test_add_user_success(fake_auth, fake_users_repo):\r
    # Arrange\r
    svc = UserService(fake_auth, fake_users_repo)\r
    email = "[email protected]"\r
    password = "secret"\r
    name = "Test User"\r
\r
    # Act\r
    result = svc.add_user(email=email, password=password, name=name)\r
\r
    # Assert\r
    assert result["status"] == "ok"\r
    assert result["user"]["email"] == email\r
    assert result["user"]["name"] == name\r
    assert result["uid"] in fake_users_repo.users\r
```\r
\r
### Parametrized Tests\r
\r
```python\r
@pytest.mark.parametrize(\r
    "email,password,name,role",\r
    [\r
        ("[email protected]", "secret", "Alice", "admin"),\r
        ("[email protected]", "p@ss", "Bob", "user"),\r
    ],\r
)\r
def test_add_user_parametrized(user_env, email, password, name, role):\r
    svc, _auth, _repo = user_env\r
\r
    # Act\r
    res = svc.add_user(email=email, password=password, name=name, global_role=role)\r
\r
    # Assert\r
    assert res["status"] == "ok"\r
    assert res["user"]["email"] == email\r
    assert res["user"]["name"] == name\r
    assert res["user"]["globalRole"] == role\r
```\r
\r
### Testing Error Scenarios with Fakes\r
\r
```python\r
@pytest.mark.parametrize("email", ["[email protected]", "[email protected]"])\r
def test_add_user_rollback_on_firestore_failure(fake_auth, fake_users_repo, email):\r
    # Arrange\r
    fake_users_repo.fail_on_upsert = True\r
    svc = UserService(fake_auth, fake_users_repo)\r
\r
    # Act & Assert\r
    with pytest.raises(RuntimeError):\r
        svc.add_user(email=email, password="secret", name="Bob")\r
\r
    # Assert rollback\r
    assert fake_auth.deleted, "Expected auth user to be deleted on Firestore failure"\r
```\r
\r
### Testing Timestamp Normalization\r
\r
```python\r
def test_list_users_normalizes_timestamps_to_iso(user_env):\r
    # Arrange\r
    svc, _auth, repo = user_env\r
    from datetime import datetime\r
    repo.users["u1"] = {\r
        "id": "u1",\r
        "email": "[email protected]",\r
        "name": "X",\r
        "globalRole": "user",\r
        "createdAt": datetime(2024, 1, 1),\r
        "updatedAt": datetime(2024, 1, 2),\r
    }\r
\r
    # Act\r
    res = svc.list_users(limit=10)\r
\r
    # Assert\r
    assert res["status"] == "ok"\r
    assert res["count"] == 1\r
    user = res["users"][0]\r
    assert isinstance(user["createdAt"], str)\r
    assert isinstance(user["updatedAt"], str)\r
```\r
\r
## Contract Testing\r
\r
### MCP Tool Registration Contract\r
\r
Test that controllers properly register tools with expected signatures:\r
\r
```python\r
# tests/test_controllers_contract.py\r
from typing import Any, Callable, Dict\r
\r
class FakeMCP:\r
    """Minimal FakeMCP for contract testing."""\r
    def __init__(self) -> None:\r
        self.tools: Dict[str, Callable[..., Any]] = {}\r
        self.meta: Dict[str, Dict[str, Any]] = {}\r
\r
    def tool(self, name: str, description: str, tags: Optional[set] = None, meta: Optional[dict] = None):\r
        def decorator(fn: Callable[..., Any]):\r
            self.tools[name] = fn\r
            self.meta[name] = {\r
                "description": description,\r
                "tags": set(tags or set()),\r
                "meta": dict(meta or {}),\r
            }\r
            return fn\r
        return decorator\r
\r
\r
class FakeUserService:\r
    """Simple fake service that records calls."""\r
    def __init__(self):\r
        self.calls = []\r
\r
    def add_user(self, **kwargs):\r
        self.calls.append(("add_user", kwargs))\r
        return {"status": "ok", "op": "add_user", "args": kwargs}\r
\r
\r
def test_users_controller_contract():\r
    # Arrange\r
    from myapp.controllers.users_controller import UsersController\r
    fake = FakeMCP()\r
    svc = FakeUserService()\r
    UsersController(fake, svc)\r
\r
    # Assert tool registration\r
    assert "add_user" in fake.tools\r
    assert "list_users" in fake.tools\r
\r
    # Act & Assert tool behavior\r
    res = fake.tools["add_user"](\r
        email="[email protected]", password="s3cr3t", name="Alice", global_role="admin"\r
    )\r
    assert res["status"] == "ok"\r
    assert res["op"] == "add_user"\r
    assert res["args"]["email"] == "[email protected]"\r
```\r
\r
### Parametrized Contract Tests\r
\r
```python\r
@pytest.mark.parametrize(\r
    "email,password,name,role",\r
    [\r
        ("[email protected]", "s3cr3t", "Alice", "admin"),\r
        ("[email protected]", "p@ssw0rd", "Bob", "user"),\r
    ],\r
)\r
def test_users_add_user_parametrized(_users_env, email, password, name, role):\r
    # Arrange\r
    fake, _ = _users_env\r
\r
    # Act\r
    res = fake.tools["add_user"](\r
        email=email, password=password, name=name, global_role=role\r
    )\r
\r
    # Assert\r
    assert res["status"] == "ok"\r
    assert res["op"] == "add_user"\r
    assert res["args"]["email"] == email\r
```\r
\r
## Repository Layer Testing\r
\r
### Testing Repository Operations\r
\r
```python\r
@pytest.fixture()\r
def repo_env(fake_institutions_repo, fake_sectors_repo):\r
    # Seed data\r
    fake_institutions_repo.upsert("inst1", {"id": "inst1", "name": "Inst One"})\r
    fake_sectors_repo.upsert_sector(\r
        "inst1", "er", {"id": "er", "name": "ER", "slug": "er", "isActive": True}\r
    )\r
    return fake_sectors_repo\r
```\r
\r
### Testing Multiple Data Scenarios\r
\r
```python\r
@pytest.mark.parametrize("rooms", [\r
    ["101"],\r
    ["201", {"name": "102", "id": "room-102"}],\r
])\r
def test_add_and_list_rooms(room_env, rooms):\r
    svc, _ = room_env\r
\r
    # Act\r
    res = svc.add_sector_rooms("inst1", "er", rooms)\r
\r
    # Assert\r
    assert res["status"] == "ok"\r
    assert res["count"] == len(rooms)\r
\r
    lst = svc.list_sector_rooms("inst1", "er", limit=10)\r
    assert lst["status"] == "ok"\r
    assert lst["count"] == len(rooms)\r
```\r
\r
### Testing Limit Behavior\r
\r
```python\r
@pytest.mark.parametrize("limit", [1, 3])\r
def test_list_rooms_limits(room_env_seeded, limit):\r
    svc = room_env_seeded\r
\r
    # Act\r
    lst = svc.list_sector_rooms("inst1", "er", limit=limit)\r
\r
    # Assert\r
    assert lst["status"] == "ok"\r
    assert lst["count"] == min(2, limit)  # 2 items seeded\r
```\r
\r
### Testing Not Found Scenarios\r
\r
```python\r
@pytest.mark.parametrize("room_id,deleted", [\r
    ("room-102", True),\r
    ("room-999", False),\r
])\r
def test_remove_rooms_parametrized(room_env_seeded, room_id, deleted):\r
    svc = room_env_seeded\r
\r
    # Act\r
    res = svc.remove_sector_room("inst1", "er", room_id)\r
\r
    # Assert\r
    assert res["deleted"] is deleted\r
    if not deleted:\r
        assert res.get("reason") == "room_not_found"\r
```\r
\r
## Integration Testing\r
\r
### Conditional Integration Tests\r
\r
Skip integration tests when external dependencies are not available:\r
\r
```python\r
# tests/test_integration_wiring.py\r
import os\r
import pytest\r
\r
# Gate this integration test on presence of credentials\r
_ENV_KEYS = (\r
    "FIREBASE_SERVICE_ACCOUNT",\r
    "GOOGLE_APPLICATION_CREDENTIALS",\r
)\r
_has_env_creds = any(os.getenv(k) for k in _ENV_KEYS)\r
\r
pytestmark = [\r
    pytest.mark.integration,\r
    pytest.mark.skipif(\r
        not _has_env_creds,\r
        reason=(\r
            "Integration test requires Firebase Admin credentials via env "\r
            "(FIREBASE_SERVICE_ACCOUNT or GOOGLE_APPLICATION_CREDENTIALS)"\r
        ),\r
    ),\r
]\r
\r
@pytest.mark.integration\r
def test_build_app_initializes_and_registers_tools():\r
    # Arrange\r
    from myapp.wiring import build_app\r
\r
    # Act\r
    app = build_app()\r
\r
    # Assert\r
    assert hasattr(app, "run")\r
```\r
\r
### Test Isolation\r
\r
Each test should be independent and not share state:\r
\r
```python\r
def test_user_created_in_one_test_not_visible_in_another(fake_auth, fake_users_repo):\r
    # Arrange\r
    svc1 = UserService(fake_auth, fake_users_repo)\r
\r
    # Act\r
    result1 = svc1.add_user(email="[email protected]", password="secret", name="User1")\r
\r
    # Assert - second test with fresh fixtures should not see this user\r
    svc2 = UserService(fake_auth, fake_users_repo)\r
    users = svc2.list_users()\r
    assert users["count"] == 1  # Only the user from this test\r
```\r
\r
## Testing Anti-Patterns to Avoid\r
\r
### Don't Mock What You Don't Own\r
\r
❌ Bad - Mocking external library:\r
\r
```python\r
@patch('firebase_admin.auth.create_user')\r
def test_add_user(mock_create_user):\r
    mock_create_user.return_value = Mock(uid="uid-1")\r
    # ... test code\r
```\r
\r
✅ Good - Use Fake for your interface:\r
\r
```python\r
def test_add_user(fake_auth, fake_users_repo):\r
    svc = UserService(fake_auth, fake_users_repo)\r
    # ... test code\r
```\r
\r
### Don't Test Implementation Details\r
\r
❌ Bad - Testing internal method calls:\r
\r
```python\r
def test_add_user(fake_auth, fake_users_repo):\r
    svc = UserService(fake_auth, fake_users_repo)\r
    svc.add_user(email="[email protected]", password="secret", name="User")\r
    assert fake_auth.created == [{"uid": "uid-1", ...}]  # Implementation detail\r
```\r
\r
✅ Good - Testing observable behavior:\r
\r
```python\r
def test_add_user(fake_auth, fake_users_repo):\r
    svc = UserService(fake_auth, fake_users_repo)\r
    result = svc.add_user(email="[email protected]", password="secret", name="User")\r
    assert result["status"] == "ok"\r
    assert result["user"]["email"] == "[email protected]"\r
```\r
\r
### Don't Skip Error Paths\r
\r
❌ Bad - Only happy path:\r
\r
```python\r
def test_add_user_success(fake_auth, fake_users_repo):\r
    # Only tests success case\r
```\r
\r
✅ Good - Test all scenarios:\r
\r
```python\r
def test_add_user_success(fake_auth, fake_users_repo):\r
    # Happy path\r
\r
def test_add_user_rollback_on_firestore_failure(fake_auth, fake_users_repo):\r
    # Error path\r
\r
def test_add_user_handles_duplicate_email(fake_auth, fake_users_repo):\r
    # Edge case\r
```\r
\r
## Running Tests\r
\r
```bash\r
# Run all tests\r
pytest\r
\r
# Run with coverage\r
pytest --cov=myapp --cov-report=term-missing\r
\r
# Run specific test file\r
pytest tests/test_user_service.py\r
\r
# Run specific test\r
pytest tests/test_user_service.py::test_add_user_success\r
\r
# Run parametrized tests with verbose output\r
pytest -v tests/test_user_service.py::test_add_user_parametrized\r
\r
# Skip integration tests\r
pytest -m "not integration"\r
\r
# Run only integration tests\r
pytest -m integration\r
\r
# Stop on first failure\r
pytest -x\r
\r
# Show local variables on failure\r
pytest -l\r
\r
# Run tests in parallel (with pytest-xdist)\r
pytest -n auto\r
```\r
\r
## Best Practices Checklist\r
\r
- [ ] Use Fake classes instead of `unittest.mock`\r
- [ ] Structure tests with explicit AAA comments\r
- [ ] Use fixtures for test setup\r
- [ ] Inject dependencies between fixtures\r
- [ ] Parametrize tests for multiple scenarios\r
- [ ] Test happy paths and error paths\r
- [ ] Test edge cases and boundaries\r
- [ ] Write contract tests for interfaces\r
- [ ] Ensure test isolation\r
- [ ] Use descriptive test names\r
- [ ] Keep tests focused on one behavior\r
- [ ] Avoid testing implementation details\r
- [ ] Test at appropriate level (unit vs integration)\r
- [ ] Mock external dependencies appropriately\r
- [ ] Maintain test coverage\r
安全使用建议
This skill is instruction-only and appears coherent for teaching and generating pytest patterns. Before using: ensure you have python3/pytest locally if you run examples; review any examples that import your app (e.g., myapp.services.user_service) so tests don't accidentally run against production resources; confirm the skill author/owner if provenance matters (registry metadata lacked a homepage while SKILL.md references a GitHub URL). There are no environment variables or install steps requested by the skill itself, so risks are limited to normal care when running test code against your codebase (avoid embedding secrets in tests or running tests that interact with live services).
功能分析
Type: OpenClaw Skill Name: clean-pytest Version: 0.1.0 The skill bundle provides a comprehensive guide and code examples for writing clean pytest tests using various patterns like Fakes, dependency injection, and contract testing. The `SKILL.md` content is purely instructional and exemplary, demonstrating testing best practices. There are no instructions for the agent to perform unauthorized actions, exfiltrate data, or execute malicious commands. The example code's use of `os.getenv` for conditional integration testing is a standard, non-malicious practice.
能力评估
Purpose & Capability
The name/description (clean pytest patterns, fakes, fixtures, contract tests) matches the content of SKILL.md. The examples and fixtures are appropriate for writing tests and reference typical application imports (e.g., myapp.services.user_service).
Instruction Scope
The runtime instructions are prose and code snippets for writing tests; they don't instruct the agent to read arbitrary system files, call external endpoints, or exfiltrate data. They only reference application-level imports and creating in-memory fakes/fixtures, which is expected for a test-patterns skill.
Install Mechanism
There is no install spec and no code files to be written or executed by the platform — instruction-only skills are low-risk. No downloads or package installs are specified.
Credentials
The skill declares no env vars or credentials. SKILL.md lists python3 under required bins in its front matter, which is proportionate for a pytest-oriented skill.
Persistence & Privilege
The skill is not marked always:true and requests no persistence or cross-skill config changes. Autonomous invocation is allowed (platform default) but presents no extra risk given the skill's limited scope.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install clean-pytest
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /clean-pytest 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v0.1.0
Initial release of clean-pytest. - Introduces guidelines and patterns for writing clean, maintainable pytest tests in Python/MCP projects. - Emphasizes using Fake-based testing over mocks for better readability and isolation. - Documents explicit Arrange-Act-Assert (AAA) structure for all tests. - Provides recommended fixture organization, including dependency injection and environment setup. - Describes contract testing, layered architecture patterns, and strategies for error testing using fakes. - Includes code examples for creating custom Fake classes and writing clear, reusable pytest fixtures.
元数据
Slug clean-pytest
版本 0.1.0
许可证
累计安装 5
当前安装数 5
历史版本数 1
常见问题

Clean Pytest 是什么?

Write clean, maintainable pytest tests using Fake-based testing, contract testing, and dependency injection patterns. Use when setting up test suites for Pyt... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 826 次。

如何安装 Clean Pytest?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install clean-pytest」即可一键安装,无需额外配置。

Clean Pytest 是免费的吗?

是的,Clean Pytest 完全免费(开源免费),可自由下载、安装和使用。

Clean Pytest 支持哪些平台?

Clean Pytest 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。

谁开发了 Clean Pytest?

由 Marco Borges(@marcoracer)开发并维护,当前版本 v0.1.0。

💬 留言讨论