← Back to Skills Marketplace
marcoracer

Clean Pytest

by Marco Borges · GitHub ↗ · v0.1.0
cross-platform ✓ Security Clean
826
Downloads
0
Stars
5
Active Installs
1
Versions
Install in OpenClaw
/install clean-pytest
Description
Write clean, maintainable pytest tests using Fake-based testing, contract testing, and dependency injection patterns. Use when setting up test suites for Pyt...
README (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
Usage Guidance
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).
Capability Analysis
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.
Capability Assessment
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.
How to Use
  1. Make sure OpenClaw is installed (local or Docker)
  2. Run the install command in chat: /install clean-pytest
  3. After installation, invoke the skill by name or use /clean-pytest
  4. Provide required inputs per the skill's parameter spec and get structured output
Version History
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.
Metadata
Slug clean-pytest
Version 0.1.0
License
All-time Installs 5
Active Installs 5
Total Versions 1
Frequently Asked Questions

What is 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... It is an AI Agent Skill for Claude Code / OpenClaw, with 826 downloads so far.

How do I install Clean Pytest?

Run "/install clean-pytest" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.

Is Clean Pytest free?

Yes, Clean Pytest is completely free (open-source). You can download, install and use it at no cost.

Which platforms does Clean Pytest support?

Clean Pytest is cross-platform and runs anywhere OpenClaw / Claude Code is available (cross-platform).

Who created Clean Pytest?

It is built and maintained by Marco Borges (@marcoracer); the current version is v0.1.0.

💬 Comments