本文へ移動
cccskills
無料GitHub で公開

backend-testing

pytest + httpx + TestClient patterns for the FastAPI backend. Dependency overrides for auth, FakeSupabase mocking, HTTPException assertions. Use when writing or reviewing backend/tests/test_*.py.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md8.9 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Backend Testing Skill

Version: 1.0 | Working Directory: backend/

Write pytest tests for $ARGUMENTS following the conventions in this repo.

Stack

  • pytest with asyncio_mode = "auto" (from pyproject.toml)
  • httpx + fastapi.testclient.TestClient — integration tests
  • monkeypatch — swap get_supabase() for FakeSupabase at the service boundary

No SQLAlchemy, no testcontainers, no real Supabase in unit tests.

Constitution (Non-Negotiable Rules)

#RuleWhy
1Tests live in backend/tests/test_*.py (pytest discovery)Matches the existing test_health.py pattern
2Test services first — they hold the logic; routers are thinRouter tests are integration sanity, not unit coverage
3Override auth deps via app.dependency_overrides[current_user] — restore in a finallyFollows test_health.py::test_admin_rejects_non_admin
4Mock Supabase at app.services.<module>.get_supabase — not globallyNarrow blast radius; other services keep their real client
5Assert snake_case detail strings for domain errors: assert r.json()["detail"] == "stock_exceeded"Matches HTTPException convention in services/products.py
6Never hit a real Supabase from a test — conftest.py stubs env with fake credentialsUnit tests must not leak
7One behavior per test — don't chain unrelated assertionsClear failure messages
8Cover the happy path + every HTTPException branch named in plan.md::Error mappingPlan is the contract

Quick Start

Service unit test

# backend/tests/test_products_service.py
import pytest
from fastapi import HTTPException

from app.services import products as products_service
from tests.fakes import FakeSupabase


def test_list_products_returns_only_active(monkeypatch):
    fake = FakeSupabase(
        table_data={"products": [
            {"id": "p1", "slug": "a", "name": "A", "is_active": True,
             "price_cents": 100, "currency": "USD", "image_url": None, "stock": 1},
            {"id": "p2", "slug": "b", "name": "B", "is_active": False,
             "price_cents": 100, "currency": "USD", "image_url": None, "stock": 1},
        ]},
    )
    monkeypatch.setattr("app.services.products.get_supabase", lambda: fake)

    result = products_service.list_products(limit=10, offset=0)
    slugs = {p["slug"] for p in result["items"]}
    assert "a" in slugs
    assert "b" not in slugs


def test_get_product_raises_not_found(monkeypatch):
    fake = FakeSupabase(table_data={"products": []})
    monkeypatch.setattr("app.services.products.get_supabase", lambda: fake)

    with pytest.raises(HTTPException) as exc:
        products_service.get_product_by_slug("missing")
    assert exc.value.status_code == 404
    assert exc.value.detail == "product_not_found"

Router integration test (auth override)

# backend/tests/test_admin_products_router.py
from fastapi.testclient import TestClient

from app.auth import AuthedUser, current_user
from app.main import app


def test_admin_list_requires_bearer():
    client = TestClient(app)
    r = client.get("/api/admin/products")
    assert r.status_code == 401


def test_admin_list_as_admin(monkeypatch):
    app.dependency_overrides[current_user] = lambda: AuthedUser(
        user_id="00000000-0000-0000-0000-000000000001",
        email="admin@example.com",
        role="admin",
    )
    try:
        # also mock the service so we don't hit Supabase
        monkeypatch.setattr(
            "app.services.products.list_products_admin",
            lambda **_: {"items": [], "total": 0},
        )
        client = TestClient(app)
        r = client.get("/api/admin/products")
        assert r.status_code == 200
        assert r.json() == {"items": [], "total": 0}
    finally:
        app.dependency_overrides.clear()

Checkout RPC test

# backend/tests/test_orders_service.py
import pytest
from fastapi import HTTPException

from app.services import orders as orders_service
from tests.fakes import FakeSupabase


def test_create_order_calls_rpc_with_snapshot(monkeypatch):
    fake = FakeSupabase(
        rpc_response={"order_id": "o1", "total_cents": 1000, "status": "paid"},
    )
    monkeypatch.setattr("app.services.orders.get_supabase", lambda: fake)

    result = orders_service.checkout(
        email="a@b.com",
        shipping_name="A",
        shipping_address={"line1": "x", "city": "y", "postal_code": "1", "country": "US"},
        items=[{"product_id": "p1", "quantity": 1}],
    )

    assert result == {"order_id": "o1", "total_cents": 1000, "status": "paid"}
    assert fake.rpc_calls[0][0] == "create_order"


def test_create_order_maps_stock_error(monkeypatch):
    fake = FakeSupabase(rpc_error="stock_exceeded")
    monkeypatch.setattr("app.services.orders.get_supabase", lambda: fake)

    with pytest.raises(HTTPException) as exc:
        orders_service.checkout(
            email="a@b.com", shipping_name="A",
            shipping_address={"line1": "x", "city": "y", "postal_code": "1", "country": "US"},
            items=[{"product_id": "p1", "quantity": 99}],
        )
    assert exc.value.detail == "stock_exceeded"

FakeSupabase (backend/tests/fakes.py)

A minimal chainable stub that covers the .table(...).select(...).eq(...).execute() and .rpc(name, payload).execute() shapes used by the services.

# backend/tests/fakes.py
from typing import Any


class _Query:
    def __init__(self, data: list[dict[str, Any]]):
        self._data = data
        self._filters: list[tuple[str, str, Any]] = []

    def select(self, *_cols: str) -> "_Query": return self
    def eq(self, col: str, val: Any) -> "_Query":
        self._filters.append(("eq", col, val)); return self
    def ilike(self, col: str, pattern: str) -> "_Query":
        self._filters.append(("ilike", col, pattern)); return self
    def order(self, *_args: Any, **_kwargs: Any) -> "_Query": return self
    def range(self, *_args: Any, **_kwargs: Any) -> "_Query": return self
    def limit(self, *_args: Any, **_kwargs: Any) -> "_Query": return self
    def single(self) -> "_Query": return self

    def execute(self) -> Any:
        out = self._data
        for op, col, val in self._filters:
            if op == "eq":
                out = [row for row in out if row.get(col) == val]
        return type("R", (), {"data": out, "count": len(out)})()


class _Rpc:
    def __init__(self, response: Any, error: str | None):
        self._response = response
        self._error = error

    def execute(self) -> Any:
        if self._error:
            raise Exception(self._error)  # services catch and map to HTTPException
        return type("R", (), {"data": self._response})()


class FakeSupabase:
    def __init__(
        self,
        *,
        table_data: dict[str, list[dict[str, Any]]] | None = None,
        rpc_response: Any = None,
        rpc_error: str | None = None,
    ) -> None:
        self._tables = table_data or {}
        self._rpc_response = rpc_response
        self._rpc_error = rpc_error
        self.rpc_calls: list[tuple[str, dict[str, Any]]] = []

    def table(self, name: str) -> _Query:
        return _Query(self._tables.get(name, []))

    def rpc(self, name: str, payload: dict[str, Any]) -> _Rpc:
        self.rpc_calls.append((name, payload))
        return _Rpc(self._rpc_response, self._rpc_error)

Extend _Query only when a service needs a method that isn't here yet — don't speculate.

Fixtures (add to conftest.py as needed)

# backend/tests/conftest.py  (append to the existing env defaults)
import pytest
from fastapi.testclient import TestClient

from app.auth import AuthedUser, current_user
from app.main import app


@pytest.fixture
def test_client():
    return TestClient(app)


@pytest.fixture
def authed_admin():
    app.dependency_overrides[current_user] = lambda: AuthedUser(
        user_id="00000000-0000-0000-0000-000000000001",
        email="admin@example.com",
        role="admin",
    )
    yield
    app.dependency_overrides.clear()


@pytest.fixture
def authed_user():
    app.dependency_overrides[current_user] = lambda: AuthedUser(
        user_id="00000000-0000-0000-0000-000000000002",
        email="user@example.com",
        role="user",
    )
    yield
    app.dependency_overrides.clear()

Running

cd backend
uv run pytest -q                        # all tests
uv run pytest tests/test_products_service.py::test_get_product_raises_not_found -vv
uv run pytest -k "admin" -vv            # by keyword

Exemplar Files

  • backend/tests/test_health.py — dependency override + TestClient pattern
  • backend/tests/conftest.py — env defaults for test isolation

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Supabase Postgres + Storage + Auth operations from FastAPI — service-role client, RLS-aware queries, additive SQL migrations under backend/db/migrations, the create_order RPC, and the product-images bucket. Use when adding tables/columns, writing queries, calling RPCs, or working with Storage.

日本語の概要は準備中です。原文の説明を表示しています。

shanraisshan/learning-x-claude22026年5月7日 更新

Build FastAPI endpoints in the layered router → service → Supabase pattern, with Pydantic v2 schemas, JWT/admin gating, and consistent HTTPException-based domain errors. Use when adding/modifying any endpoint under backend/.

日本語の概要は準備中です。原文の説明を表示しています。

shanraisshan/learning-x-claude22026年5月7日 更新

Drive Chrome via the claude-in-chrome MCP to exercise plan.md::Verify steps for a shipped feature. Loads deferred MCP tool schemas, asserts console/network cleanliness, records a GIF for the PR. Used by the browser-verifier agent.

日本語の概要は準備中です。原文の説明を表示しています。

shanraisshan/learning-x-claude22026年5月7日 更新

Pre-PR code review checklist for this repo. Plan alignment, security gates, layering, dead code, API-surface completeness. Used by the code-reviewer agent.

日本語の概要は準備中です。原文の説明を表示しています。

shanraisshan/learning-x-claude22026年5月7日 更新

Integrate with the FastAPI backend through the typed wrapper in frontend/lib/api.ts, attach Supabase JWTs for admin calls, and handle errors via ApiError. Use when adding/modifying API calls or wiring auth.

日本語の概要は準備中です。原文の説明を表示しています。

shanraisshan/learning-x-claude22026年5月7日 更新

Build pages and components in the Next.js 15 App Router (RSC by default, "use client" only when needed), Tailwind for styling, typed props, and the existing CartProvider for cart state. Use when creating or modifying pages, layouts, components, or forms in frontend/.

日本語の概要は準備中です。原文の説明を表示しています。

shanraisshan/learning-x-claude22026年5月7日 更新

shanraisshan のスキルをすべて見る

このスキルの問題を報告する