Testing overview
The framework expects three kinds of tests:
- Unit tests — pure functions, small service methods against
db_session.uv run pytest <path>. - Integration tests — endpoints via
client/authenticated_client. Stillpytest. - E2E tests — Playwright-driven browser tests marked
e2e, excluded from default runs.
The default pytest invocation pins -m 'not e2e and not perf' (your app's pyproject.toml), so a normal test run never touches the Playwright suite or the benchmarks — run e2e explicitly via uv run pytest -m e2e tests/e2e.
Quick reference
| Command | What it does |
|---|---|
uv run pytest | Run every Python test (e2e excluded). |
uv run pytest path/to/test_file.py::test_name | Single test. |
uv run pytest -k "create_order" | All tests matching a name pattern. |
uv run pytest --lf | Last failed only. |
uv run pytest -x | Stop at first failure. |
uv run pytest -v -s | Verbose + capture off (see print). |
uv run pytest -m e2e tests/e2e | Playwright suite against localhost:8000. Requires make dev running. |
npm test (or npx vitest run) | Run every JS test (from the repo root). |
npx vitest run <path> | Single JS test file. |
Postgres suite
make test-py-pg runs the Python suite on Postgres in parallel, with 4 workers by default (PYTEST_WORKERS overrides this; 0 runs serially). Set SM_TEST_PG_URL to a disposable test database URL; the default database is sm_test. Each xdist worker uses its own <database>_gw<N> database (for example, sm_test_gw0), created if needed; the role needs CREATEDB to create them. Serial runs use the named database itself.
Tables are emptied between tests and the schema is rebuilt only when changed. The target sets SM_TEST_DATABASE_URL for the fixtures and disables AnyIO's pytest plugin with -p no:anyio so asyncpg connections stay on their event loop. See Fixtures for fixture behavior.
Async mode
The root pyproject.toml sets asyncio_mode = "auto". Async tests don't need @pytest.mark.asyncio — any async def test_* is picked up automatically. You still need it on fixtures that are async generators.
File layout
conftest.py # intentionally thin — shared fixtures ship in the simple_module_test plugin
framework/testing/ # simple_module_test plugin: app, db_session, client, authenticated_client fixtures
framework/<pkg>/tests/ # tests against each framework package (core, db, hosting, cli, testing)
host/tests/ # host-level tests
modules/<name>/tests/ # per-module pytest tests
tests/
├── integration/ # cross-module integration tests
├── benchmarks/ # perf benchmarks (marked `perf`)
└── e2e/ # Playwright tests (marked `e2e`)These directories are enumerated in testpaths in the root pyproject.toml. Any conftest.py under a subtree can add more fixtures; they cascade down but don't leak sideways. Keep module-specific fixtures in the module's modules/<m>/tests/conftest.py.
What good tests look like
- Exercise behavior, not implementation. A test that asserts
assert service._private_method_called_twiceis a red flag — rewrite to assert on the observable outcome. - Use the real DB (SQLite in-memory via
db_session). Mocking SQLAlchemy is almost always wrong. - Avoid mocking your own code. Mock external boundaries (HTTP, SMTP, queue). For internal calls, instantiate the collaborator.
- Use
authenticated_clientfor endpoint tests that need auth — don't reinvent the session cookie.
Coverage expectations
There's no enforced coverage percentage, but:
- Every new endpoint gets at least one happy-path test.
- Every new service method with branching logic gets branch coverage.
- Bug fixes land with a regression test.
- Migration files don't need tests — but if the data-migration logic is non-trivial, extract the logic into a module-level function and test that.
JS / TSX tests
Vitest + Testing Library, configured in the repo-root vitest.config.ts and vitest.setup.ts. The config's include globs cover packages/** and host/client_app/**, so tests live next to the source there:
packages/ui/src/components/StatCard.test.tsx
host/client_app/<something>.test.tsxRun all: npm test (or npx vitest run). Watch: npx vitest. Single file: npx vitest run path/to/StatCard.test.tsx.
Use @testing-library/react's render and query helpers:
import { render, screen } from "@testing-library/react";
import Browse from "../Browse";
it("shows the empty state", () => {
render(<Browse orders={[]} />);
expect(screen.getByText(/no orders yet/i)).toBeInTheDocument();
});For components that depend on useT / usePage, mock the source module with vi.mock("@simple-module-py/i18n", ...) / vi.mock("@inertiajs/react", ...) — see packages/ui/src/components/LocaleSwitcher.test.tsx.
Flaky-test checklist
Before marking a test flaky, check:
- Time-dependent? Use
freezegunor inject a clock. - Order-dependent? Run
pytest -p no:randomlyto disable reordering and see if the issue is real ordering coupling. - Async-order-dependent?
awaiteverything that returns a coroutine, includingsession.flush()andsession.refresh(). - Shared state? Check for
scope="module"fixtures that should bescope="function".