Overview
Run Litestore's tests and choose where a new test belongs.
Litestore's tests make sure that money, stock and checkout keep working when you change the code. The sections below cover how to run them, what the important ones check, and where a new test should go.
Test types
Litestore has unit, integration and end-to-end tests, and each kind has its own script.
| Type | Script | Where the tests are | What they need |
|---|---|---|---|
| Unit | bun run test | test/unit/ and next to the modules they test | Nothing. The database is mocked. |
| Integration | bun run test:integration | test/integration/ | A local Postgres database called litestore_test. |
| End-to-end | bun run test:e2e | test/e2e/ | A local Postgres database called litestore_e2e. Stripe and S3 are replaced by local mocks. |
We keep them apart on purpose. Unit tests should be cheap enough to run all the time while you work. Integration tests are for behavior that only a real database proves, and end-to-end tests prove that a whole journey works in the browser.
The repository currently has 93 *.test.ts files. 63 of them are in test/unit/, 20 are in test/integration/, one checks the order factory in test/factories/, and the other nine sit next to the modules they test under modules/. There are eight end-to-end journey files in test/e2e/, and docs/journeys.md walks each journey through the code it exercises.
Test commands
bun run testThis runs vitest run, which runs the unit tests once. verify and CI run the same command.
bun run test runs Vitest, not bun test. The tests depend on the Vitest configuration, so Bun's own test runner won't produce the same result.
Unit tests
vitest.config.ts picks up every *.test.ts and *.test.tsx file and leaves out these paths:
test/integration/**
**/.claude/**
**/node_modules/**It sets SKIP_ENV_VALIDATION=1 and uses the node environment. It also points server-only at test/server-only-stub.ts.
As a result, the unit tests run without Postgres, Redis, S3 credentials or any other infrastructure.
Integration tests
vitest.integration.config.ts points the tests at this database:
postgresql://postgres:postgres@localhost:5432/litestore_testSet TEST_DATABASE_URL to use a different one.
The integration config also sets fileParallelism: false. The suites share one database and truncate or change shared tables. If the files ran at the same time, one test could break another.
Each integration test gets 30 seconds instead of the default 10.
End-to-end tests
playwright.config.ts starts next dev on port 3100 against the litestore_e2e database. It starts a local Stripe mock and a local S3 mock, so the tests never reach a real payment provider or bucket. No AI provider key is set, so the journeys cover the admin as it works without AI.
The journeys cover a shopper buying, cancelling and getting a refund, search and the customer account, messaging, reviews, the creator programme, every admin section, the assistant on the admin home page and the primary action in each admin area (create and edit a product, create a discount, a collection, a category and a page, invite a team member, refund an order).
Single files
When you work on one area, run its file or directory first. You don't need to wait for the full suite after every small change.
# one file
bun run vitest run test/unit/refund-cap.test.ts
# one directory
bun run vitest run test/unitContract tests
A contract test checks a behavior that other parts of Litestore are allowed to rely on. Litestore has contract tests at two levels.
Application invariants
test/unit/money-laws.test.ts runs a fixed set of amounts, rates and tax settings through the real calculation code. The inputs are fixed, and the test doesn't generate random input. test/unit/invariants.test.ts covers the runtime invariant guards in modules/operations/invariants.ts (numeric, stock and money checks) in the same style.
| Rule | What the test proves |
|---|---|
subtotal + tax === total | The parts always add up to the total. |
| Inclusive tax | It is extracted from the price, never added on top. |
| Exclusive tax | It adds tax. |
calculateExclusiveTax | It is non-negative and monotonic. |
sum() | It does not depend on operand order, and it throws when currencies do not match. |
| Order breakdown | Subtotal minus discount plus tax plus shipping equals the total. |
| Refund clamp | A refund never exceeds what was charged. |
| Cash ledger and discount stacking | The ledger projection reconciles, and stacked discounts stay within their bound. |
These tests are small, but checkout, pricing and payments all depend on the rules they check. A test of one example can miss a broken rule. These tests try many inputs, so they catch a whole kind of failure.
Database guarantees
The integration tests check rules that Postgres itself enforces.
test/integration/db-guarantees.test.ts creates a paid order. Then it tries to write data the database must reject, such as a refund larger than the order allows or stock movement numbers that don't add up.
The test doesn't check which application function rejected the write. What it proves is that the database will not accept the invalid data.
Other suites in test/integration/ cover stock reservations, price resolution, refunds, commission and order flows that need a real database.
Contract tests check guarantees, not implementations
There are fewer of them and they run slower. Never delete one to make a build pass.
Production code
A test imports the production function and runs it. If a test file defines its own copy of the function instead, the test checks the copy, and it keeps passing when the real function in the app is broken.
Rendering tests
Litestore has no component rendering tests. The repository has no @testing-library dependency, no snapshot assertions and no toMatchSnapshot calls. The end-to-end tests check the UI instead, by using it the way a shopper or an admin would.
A snapshot shows that the markup changed, but it can't show whether the new UI is right, and you would have to update it every time the design changes.
When a component's behavior can be written as data or configuration, Litestore tests that data directly. For example, modules/storefront/feed/facets.test.ts checks that facet bindings map to separate URL parameters. It also checks that those parameters parse back into the filter shape the feed accepts.
Litestore doesn't enforce a line-coverage percentage. vitest.config.ts has a coverage block that uses the v8 provider, but @vitest/coverage-v8 isn't installed.
Pull requests
bun run verifyverify runs typecheck, lint and test one after another and stops at the first failure. GitHub Actions runs the same script on every pull request and every push to main, so the checks you run locally match the checks that determine whether a change can merge.
CI doesn't run the integration or end-to-end tests, because they need a database. If your change touches money, stock, checkout or a journey the end-to-end tests cover, run bun run test:integration and bun run test:e2e yourself before you open the pull request.
Test placement
When you add a test, consider what could break in the code you changed, and test it at the lowest layer that still proves it works.