# Squid Testing Python

> Write and evaluate effective Python tests using pytest. Use when writing tests, reviewing test code, debugging test failures, or improving test coverage.

- Skill: `iusztinpaul/squid-testing-python` (Agent Skill)
- Install (CLI): `npx skillmds@latest add iusztinpaul/squid-testing-python`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iusztinpaul/squid-testing-python/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: iusztinpaul (https://skillmd.com/u/iusztinpaul)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iusztinpaul/squid-testing-python

---


# Writing Effective Python Tests

## Test Structure

### Mirror the module layout

Keep a one-to-one relationship between test files and the modules they cover: `myapp/service.py` → `tests/.../test_service.py`. This makes the test for any given module obvious and keeps coverage gaps visible.

### Follow AAA (Arrange, Act, Assert)

Structure each test body in three beats — set up inputs (Arrange), call the thing under test (Act), then assert on the result. Keep them in that order; don't interleave more setup after the act.

### Atomic unit tests

Each test should verify a single behavior. The test name should tell you what's broken when it fails. Multiple assertions are fine when they all verify the same behavior.

```python
# Good: Name tells you what's broken
def test_user_creation_sets_defaults():
    user = User(name="Alice")
    assert user.role == "member"
    assert user.id is not None
    assert user.created_at is not None

# Bad: If this fails, what behavior is broken?
def test_user():
    user = User(name="Alice")
    assert user.role == "member"
    user.promote()
    assert user.role == "admin"
    assert user.can_delete_others()
```

### Use parameterization for variations of the same concept

```python
import pytest

@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("World", "WORLD"),
    ("", ""),
    ("123", "123"),
])
def test_uppercase_conversion(input, expected):
    assert input.upper() == expected
```

Don't parameterize unrelated behaviors — if the test logic differs, write separate tests.

## Project-Specific Rules

### Imports at module level

Put ALL imports at the top of the file. Do not import inside test function bodies.

```python
# Correct
import pytest
from myapp.service import do_work

def test_something():
    assert do_work() is not None

# Wrong - no local imports
def test_something():
    from myapp.service import do_work  # Don't do this
    ...
```

### Async tests

If the project sets `asyncio_mode = "auto"` in `pyproject.toml`, write async tests without decorators:

```python
# Correct (when asyncio_mode = "auto")
async def test_async_operation():
    result = await some_async_function()
    assert result == expected
```

Otherwise, mark explicitly with `@pytest.mark.asyncio`.

### Inline snapshots for complex data

If the project uses `inline-snapshot`, use it for JSON schemas and complex structures:

```python
from inline_snapshot import snapshot

def test_schema_generation():
    schema = generate_schema(MyModel)
    assert schema == snapshot()  # Will auto-populate on first run
```

Commands:
- `pytest --inline-snapshot=create` - populate empty snapshots
- `pytest --inline-snapshot=fix` - update after intentional changes

## Fixtures

### Share setup through fixtures, not setup/teardown methods

Put shared fixtures in `conftest.py` so they're available across test files without imports. Use fixtures (and their `yield`-based teardown) for setup and cleanup — avoid xUnit-style `setUp`/`tearDown` methods.

### Prefer function-scoped fixtures

```python
@pytest.fixture
def client():
    return Client()

def test_with_client(client):
    result = client.ping()
    assert result is not None
```

### Use `tmp_path` for file operations

```python
def test_file_writing(tmp_path):
    file = tmp_path / "test.txt"
    file.write_text("content")
    assert file.read_text() == "content"
```

## Mocking

### Mock at the boundary

Use `pytest-mock`'s `mocker` fixture (preferred) or `unittest.mock` patches.

```python
from unittest.mock import AsyncMock

async def test_external_api_call(mocker):
    mock = mocker.patch("mymodule.external_client.fetch", new_callable=AsyncMock)
    mock.return_value = {"data": "test"}
    result = await my_function()
    assert result == {"data": "test"}
```

### Don't mock what you own

Test your code with real implementations when possible. Mock external services (HTTP APIs, third-party SDKs), not your own internal classes.

### Don't write unit tests against infrastructure components

Orchestrators, model-serving runtimes, observability clients, and similar infrastructure should be exercised via **integration tests**, not unit tests with mocks of their internals.

## Test Naming

Test files must be named `test_*.py` and test functions `test_*` so pytest discovers them. Beyond that, use descriptive names that explain the scenario:

```python
# Good
def test_login_fails_with_invalid_password():
def test_user_can_update_own_profile():
def test_admin_can_delete_any_user():

# Bad
def test_login():
def test_update():
def test_delete():
```

## Running Tests

Prefer project Make targets when available: `make unit-tests`, `make integration-tests`, `make tests`. Otherwise `uv run pytest -n auto` (parallel). The suite must finish with 0 warnings.

