Skip to content
Back to skills

Pytest

ASecurity

pytest is the standard Python test framework: plain assert statements, fixtures for setup and teardown, parametrized tests and a large plugin ecosystem. Use when a user asks to write unit tests, set up test fixtures, mock dependencies, run async tests, measure coverage, run tests in parallel, or practice test-driven development in Python.

  • 142 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 6, 2026
testingpythongobashsqlexpressfastapispringtestinggitapi

Works with

  • terminal
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill pytest --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pytest?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pytest
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-pytest/badge)](https://www.skillsdirectory.com/skills/terminalskills-pytest)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pytest
description: >-
  pytest is the standard Python test framework: plain assert statements,
  fixtures for setup and teardown, parametrized tests and a large plugin
  ecosystem. Use when a user asks to write unit tests, set up test fixtures,
  mock dependencies, run async tests, measure coverage, run tests in
  parallel, or practice test-driven development in Python.
license: Apache-2.0
compatibility: 'Python 3.10+ (pytest 9); pytest-asyncio, pytest-cov, pytest-mock and pytest-xdist are optional plugins'
metadata:
  author: terminal-skills
  version: 1.1.0
  category: development
  repository: https://github.com/pytest-dev/pytest
  tags:
    - pytest
    - testing
    - python
    - tdd
    - fixtures
---

# pytest

## Overview

pytest collects functions named `test_*` in files named `test_*.py` (or `*_test.py`), runs them, and rewrites plain `assert` statements so a failure shows both sides of the comparison. Fixtures provide setup and teardown, `parametrize` runs one test over many inputs, and plugins add async support, coverage, mocking and parallel runs. This skill targets pytest 9.x (checked against 9.1) with pytest-asyncio 1.x.

## Instructions

### Step 1: Install and configure

```bash
python -m venv .venv && source .venv/bin/activate
pip install pytest pytest-asyncio pytest-cov pytest-mock pytest-xdist
```

Put configuration in `pyproject.toml` (`[tool.pytest.ini_options]`) or, since pytest 9, in a native `pytest.toml`:

```toml
# pytest.toml
[pytest]
pythonpath = ["."]            # lets tests import top-level modules such as `app`
testpaths = ["tests"]
asyncio_mode = "auto"         # pytest-asyncio: no @pytest.mark.asyncio on every test
markers = ["slow: tests that take more than a second"]
strict = true                 # unknown markers and options become errors
```

Without `pythonpath` (or an installed package), `from app import ...` fails with `ModuleNotFoundError` when pytest is run from the project root.

### Step 2: Basic tests

```python
# tests/test_users.py
import pytest
from app.services.users import create_user, validate_email

def test_create_user_returns_user_object():
    user = create_user(name="Priya Nair", email="priya@acme.io")
    assert user.name == "Priya Nair"
    assert user.id is not None

def test_validate_email_rejects_invalid():
    assert validate_email("not-an-email") is False
    assert validate_email("user@") is False

class TestUserService:
    """A class only groups tests; no base class or self.assert* methods needed."""

    def test_duplicate_email_raises(self):
        create_user(name="Priya Nair", email="priya@acme.io")
        with pytest.raises(ValueError, match="Email already exists"):
            create_user(name="Tom Berg", email="priya@acme.io")
```

### Step 3: Fixtures

```python
# tests/conftest.py — shared by every test in this directory and below
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.models import Base, User

@pytest_asyncio.fixture          # async fixtures need this decorator in strict mode
async def db():
    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    async with async_sessionmaker(engine, expire_on_commit=False)() as session:
        yield session            # code after yield is the teardown
    await engine.dispose()

@pytest_asyncio.fixture
async def sample_user(db):
    user = User(name="Priya Nair", email="priya@acme.io", role="member")
    db.add(user)
    await db.commit()
    return user

@pytest.fixture
def api_client(db):
    from fastapi.testclient import TestClient
    from app.main import app
    from app.dependencies import get_db

    app.dependency_overrides[get_db] = lambda: db
    yield TestClient(app)
    app.dependency_overrides.clear()
```

That example needs `pip install "sqlalchemy[asyncio]" aiosqlite`. Built-in fixtures worth knowing: `tmp_path` (a fresh directory per test), `monkeypatch` (set env vars and attributes, undone automatically), `capsys` and `caplog` (captured output and log records). A fixture's `scope` can be `function` (default), `class`, `module`, `package` or `session`; widen it only for expensive, read-only resources.

### Step 4: Parametrize

```python
@pytest.mark.parametrize(
    "plan,expected_price",
    [("free", 0), ("starter", 29), ("pro", 79), pytest.param("enterprise", 199, id="enterprise-list-price")],
)
def test_calculate_price(plan, expected_price):
    assert calculate_price(plan) == expected_price
```

pytest 9 also ships subtests (a `subtests` fixture) for looping over cases inside one test without stopping at the first failure:

```python
def test_slugify_variants(subtests):
    for raw, slug in [("Hello World", "hello-world"), ("  Spaces  ", "spaces")]:
        with subtests.test(raw=raw):
            assert slugify(raw) == slug
```

### Step 5: Mocking

```python
# tests/test_notifications.py — pytest-mock provides the `mocker` fixture
from unittest.mock import AsyncMock

async def test_send_welcome_email(mocker, sample_user):
    send = mocker.patch("app.services.email.send_email", new_callable=AsyncMock,
                        return_value={"id": "msg_8841"})

    result = await send_welcome_email(sample_user.id)

    send.assert_awaited_once_with(to="priya@acme.io", subject="Welcome!", template="welcome")
    assert result["id"] == "msg_8841"
```

Patch the name where it is looked up (`app.services.notifications.send_email`, if that module did `from ... import send_email`), not where it is defined.

### Step 6: Run

```bash
pytest                                  # everything under testpaths
pytest tests/test_users.py::test_validate_email_rejects_invalid   # one test
pytest -x --lf                          # stop at first failure; rerun last failures
pytest -k "create and not duplicate"    # select by expression
pytest -m "not slow"                    # select by marker
pytest --cov=app --cov-report=term-missing   # coverage (pytest-cov)
pytest -n auto                          # parallel workers (pytest-xdist)
pytest -ra                              # summary of skipped/xfailed reasons
```

## Examples

### Example 1: Add tests for a pricing function

User request: "Write pytest tests for calculate_discount in app/pricing.py, including bad input."

```python
# tests/test_pricing.py
import pytest
from app.pricing import calculate_discount

@pytest.mark.parametrize("total,code,expected", [(100.0, "SPRING10", 90.0), (100.0, None, 100.0), (0.0, "SPRING10", 0.0)])
def test_discount_applies(total, code, expected):
    assert calculate_discount(total, code) == pytest.approx(expected)

def test_unknown_code_raises():
    with pytest.raises(ValueError, match="Unknown discount code"):
        calculate_discount(50.0, "NOPE")
```

Run `pytest tests/test_pricing.py -q`; expect `4 passed` (three parametrized cases plus the error test).

### Example 2: Fix "async def functions are not natively supported"

User request: "My async tests fail with 'async def functions are not natively supported'."

pytest does not run coroutines on its own. Install the plugin and set the mode:

```bash
pip install pytest-asyncio
```

Add `asyncio_mode = "auto"` to `pytest.toml`, or keep the default strict mode and decorate each test with `@pytest.mark.asyncio` and each async fixture with `@pytest_asyncio.fixture`. Re-run `pytest -q`: the tests now pass.

## Guidelines

- Use plain `assert`; use `pytest.approx` for floats and `pytest.raises(..., match=...)` for exceptions.
- In strict asyncio mode (the default), an async fixture declared with plain `@pytest.fixture` errors with "requested an async fixture ... no plugin or hook that handled it"; use `@pytest_asyncio.fixture` or switch to `asyncio_mode = "auto"`.
- Fixtures with `yield` clean up even when the test fails; no try/finally needed.
- Keep tests independent: no reliance on order, shared mutable module state or a leftover database. In-memory SQLite suits unit tests; use the real database engine for integration tests.
- Register custom markers in configuration; with `strict = true` (or `--strict-markers`) a typo fails at collection instead of silently deselecting tests.
- Parallel runs (`-n auto`) need tests that do not share files, ports or database rows.
- Do not mock what you own and can run cheaply; mock network calls, clocks and paid APIs.
- `unittest.TestCase` classes also run under pytest, but fixtures and parametrize do not apply to their methods.

Files in this skill

  • SKILL.md5.3 KB
  • _scores.json1.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…