Testing¶
Vectis uses pytest with pytest-asyncio. Tests run against a real PostgreSQL database (not SQLite) for production parity with JSONB, BigInteger PKs, and async drivers.
Running Tests¶
cd vectis/backend
pytest # all tests
pytest tests/test_strategy.py -v # single file
pytest -k "test_resolve" # pattern match
Test Database Setup¶
The conftest.py fixture creates all tables at session start and drops them after. A dedicated vectis_test database is configured via DATABASE_URL env var. reset_engine() clears the app's singleton so it picks up the test URL.
Note
Set DATABASE_URL to a separate vectis_test database. The fixture runs Base.metadata.create_all at session start and drop_all at teardown.
Shared Fixtures¶
@pytest.fixture
def event_bus():
return EventBus()
@pytest.fixture
def strategy_resolver():
return StrategyResolver()
@pytest.fixture
def staff_context():
return RequestContext(
user_id=1, user_type="staff", is_authenticated=True,
is_staff=True, permissions=["*"],
)
Testing Services¶
@pytest.mark.asyncio
async def test_create_product():
factory = get_session_factory()
async with factory() as session:
svc = ProductService(session)
product = await svc.create(name="Widget", slug="widget", status="draft")
assert product.id is not None
assert product.name == "Widget"
await session.commit()
Testing Strategies¶
class MockTax:
pass
def test_register_and_resolve(strategy_resolver):
impl = MockTax()
strategy_resolver.register(MockTax, impl, name="default")
assert strategy_resolver.resolve(MockTax) is impl
def test_resolve_all(strategy_resolver):
a, b = MockTax(), MockTax()
strategy_resolver.register(MockTax, a, name="state")
strategy_resolver.register(MockTax, b, name="county")
assert len(strategy_resolver.resolve_all(MockTax)) == 2
Testing the EventBus¶
@pytest.mark.asyncio
async def test_priority_ordering(event_bus):
order = []
async def high(e): order.append("high")
async def low(e): order.append("low")
event_bus.subscribe("test", low, priority=100)
event_bus.subscribe("test", high, priority=10)
await event_bus.emit(Event(type="test"))
assert order == ["high", "low"]
Integration Tests¶
@pytest.mark.asyncio
async def test_health_check(client):
resp = await client.post("/graphql", json={"query": "{ health }"})
assert resp.status_code == 200
assert resp.json()["data"]["health"] == "Vectis Commerce API is healthy"
Warning
Integration tests share the database. Clean up test data or use unique identifiers to avoid collisions.
Test Organization¶
tests/
├── conftest.py # Shared fixtures
├── test_strategy.py # Strategy resolver unit tests
├── test_events.py # EventBus unit tests
├── test_pricing.py # Pricing module tests
├── test_cart_order_flow.py # Cart-to-order integration
├── test_graphql_api.py # GraphQL resolver tests
└── test_rbac.py # Permission and role tests
Local Check Gate¶
The single source of truth for "is this branch safe to push?" is make check, run inside the API container:
The check (~25s) runs ruff, mypy, the resolver-duplicate AST guard, the zero-extension-imports AST guard (Decided #233), the schema-drift guard, and the full 1,670-test pytest suite. CI is currently disabled in favour of this local gate, so push directly to main after make check passes.
The check is fast because pytest reuses the database fixture; tests are written assuming a real PG, so don't reach for a SQLite fallback.
End-to-End Tests (Playwright)¶
The storefront and admin have Playwright suites under each app's e2e/ directory. The CI scaffolding (now optional) bootstraps migrations and the Temporal namespace before the probe runs (commit 89ac32e):
docker compose run --rm api-migrate alembic upgrade head
docker compose run --rm temporal-init
npx playwright test
In dev, prefer running the Playwright suite against the docker-compose stack from the host with the storefront and admin both up.
Regression Guard (Phase D)¶
tests/test_module_imports.py imports every backend module at collect-time so circular imports, missing exports, and broken module surfaces fail loudly during make check rather than at production boot.