跳到正文
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.

中文简介正在准备中。当前显示原文说明。

集成工具Supabase
查看安装方法

包含的文件(1)

  • SKILL.md8.9 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

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

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

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 的全部技能

报告此技能的问题