Choosing test layers — Vitest, pytest, Cypress, and Playwright
Separate unit, integration, browser, and production smoke checks, then assign Vitest, pytest, Cypress, and Playwright without duplicating responsibilities.
Table of contents
Classify tests by which boundaries they actually cross, not by tool name. Launching a browser does not automatically make a test end to end, and extensive mocking does not automatically make it a unit test. Start with the fact you need to learn when the test fails, then choose the narrowest layer.
Widening the verification boundary
Check input and output quickly.
Check framework boundaries and collaborators.
Check contracts against a real database, cache, or broker.
Check a user's critical action and visible state.
Check deployed read paths and readiness only.
Choose tools by responsibility
| Tool | Strong area | Typical checks | Common misuse |
|---|---|---|---|
| Vitest | Fast TypeScript and React unit/component tests | Pure functions, hooks, render states, mocked boundaries | Treating jsdom as proof of real browser behavior |
| pytest | Python unit, API, and scheduler tests | Parsers, FastAPI contracts, time zones, retries, DB adapters | Treating mock call counts as proof of DB constraints |
| Cypress | Browser-resident user flows with network control | Forms, routing, failed responses, retry UI | Rechecking every internal branch through a browser |
| Playwright | Multi-browser, multi-context, multi-tab, and API E2E | Authentication, popups, multiple users, route smoke | Treating status-only page checks as proof of business success |
Cypress and Playwright are not merely rival names. Choose them for the interaction you need. If one product uses both, fix their ownership explicitly—for example, Cypress for screen-focused regression and Playwright for multi-context flows and deployment smoke.
Do not repeat the same defect at every layer
A login button's disabled condition belongs in a component test. Whether a real cookie reaches the next request belongs in browser E2E. Session-store expiry belongs in an integration test.
Duplicate and complementary tests
Recreate the same conditional in both Vitest and Cypress.
Vitest checks state transitions; Cypress checks user action and network failure.
Check health, readiness, and public reads instead of replaying write scenarios.
A failure should narrow the cause to one layer. If every test starts a browser and a real database, the suite becomes slow and the broken contract becomes harder to identify.
Mocks versus real dependencies
Mocks are useful for checking how your code calls a dependency. Properties guaranteed by an external system—UNIQUE constraints, transaction isolation, SQL syntax, or Redis TTL—need a real instance.
Mocks are enough
parser input → output
dependency failure → visible error state
retry count and backoff cap
A real dependency is required
concurrent INSERT deduplication
transaction rollback
index-backed query plan
cache expiry and source reload
Even with Testcontainers or isolated Compose, never use a production database as the test target. Finish destructive and write-path verification in DEV or an ephemeral instance; run only non-destructive smoke checks in production.
Limit browser-test cost
Browser tests cost execution time, image downloads, CI traffic, and investigation effort. Keep the boundary deliberate.
- Run critical user paths and changed areas on each pull request.
- Move the full browser matrix to scheduled or release gates.
- Do not install E2E browser binaries in production application images.
- Capture screenshots, traces, and network logs on failure, while removing personal data and tokens.
- Keep production smoke checks GET/HEAD-oriented and block mutations by default.
Failure states are part of the contract
A green happy path can still leave users with a blank screen during an outage. Verify at least these states:
- duplicate submission is blocked while loading
- 4xx responses explain how to correct input
- 5xx and network failures offer a retry path
- empty results differ from failed retrieval
- partial success is not presented as total success
- retries do not duplicate the same write
Completion criteria
Test count is not a quality metric. You should be able to explain which evidence protects each changed contract and risk.
- The narrowest test reproduces the defect and passes after the fix.
- Integration tests prove guarantees owned by real dependencies.
- Browser tests protect critical user paths.
- Build, lint, and type checks pass.
- Health, readiness, and public smoke pass after deployment.
- Production writes and paid external calls remain disabled by default.
Next
- testcontainers
- vitest-philosophy
- e2e-equivalence-manifest
See the official documentation for Vitest, pytest, Cypress, and Playwright.