Integration
Run Litestore's integration tests, and add a new test there when it needs a real database.
Most of Litestore's tests run against stubs. The tests in test/integration/ run against a real Postgres database instead, because they check behavior that the database itself enforces. The sections below cover how to run them and when a new test belongs with them.
Separate configuration
The integration tests are in test/integration/ and run with their own Vitest config, vitest.integration.config.ts.
Keeping them in a separate lane means the unit lane stays fast and needs no database. You shouldn't need Postgres on your machine to run tests in watch mode. But a test that proves a Postgres constraint means nothing without Postgres.
Test setup
Local Postgres
You need the local Postgres from compose.yml.
Test database
Create the test database and apply the migrations to it. You only do this once.
docker compose exec postgres psql -U postgres -c "CREATE DATABASE litestore_test"
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/litestore_test bun run db:deployUse db:deploy, not db:migrate: db:migrate only writes a new migration file and doesn't apply anything.
Test run
bun run test:integrationThe script runs:
vitest run --config vitest.integration.config.tsThe lane reads its database from TEST_DATABASE_URL, so it never points at your development database by accident.
Database guards
test/integration/setup.ts runs before each test file's imports. It sets the connection before Prisma is created, using this default:
postgresql://postgres:postgres@localhost:5432/litestore_testIf you set TEST_DATABASE_URL, it uses that instead, for both the pooled and the unpooled URL.
Before it does any of this, it checks the URL. If the URL doesn't match litestore_test, it throws and stops. The config also sets the same URL a second time, so no code that runs between loading the config and running the setup can read your development database URL.
The reason for these checks is the next line in the file: every test starts from an empty database. Before each test, truncateAll from test/integration/seed-rows.ts runs a single TRUNCATE … RESTART IDENTITY CASCADE over every non-Prisma table in the public schema.
If that ran against your development data, it would wipe it. So the setup won't start unless the database name says it's the test database.
Every test empties the database
The integration lane truncates all tables before each test. It only runs against
a database whose URL matches litestore_test.
Sequential files
The config sets fileParallelism: false. The suites share one database and empty it between tests. If two files ran at once, one would delete the other's rows in the middle of a check.
Each test gets 30 seconds instead of the default 10, because a real transaction against a real database takes longer than a stubbed call.
Suites
test/integration/ has 20 suites. Besides the three described here, they cover stock levels, order flows, cancellations, refund requests, coupon writes, commission and campaign statistics. These three show the main reasons a test needs a real database.
db-guarantees.test.ts
This suite proves that the CHECK constraints and triggers really reject the writes they are meant to reject. Examples are a refund larger than the order, an edit to a ledger row, or a stock movement whose numbers don't add up.
It creates a paid order and then tries writes that the database must reject. The money checks try to write each of these.
| Write the suite tries | What Postgres must do |
|---|---|
| An order with no base-currency amount, even through raw SQL | Reject it. |
| An order whose refunded amount is larger than its total | Reject it. |
| Refunds in the payment ledger that add up to more than the order total | Reject them. |
| Approved refund requests that add up to more than the order total | Reject them. |
| A ledger row in a different currency from its order | Reject it. |
| An approval for more than the stored refund request amount | Reject it. |
The stock checks try to write a movement that doesn't match the current inventory row. They also check the opposite case: the canonical stock writer is allowed through, because its inventory and ledger numbers match.
That second check matters. Without it, a database that rejected every write would pass the suite.
The checks only look at whether the database rejected the write, not which application function rejected it. The suite uses raw SQL on purpose so the application code isn't involved at all.
adjust-inventory.test.ts
This suite proves that when several checkouts try to buy the last unit at the same time, the guarded update lets exactly one of them through.
It runs the transactional inventory code against real Postgres. It checks that the guarded decrement lowers stock correctly against a real row. When several buyers race for the last unit, it checks that exactly one wins and stock never goes below zero. It checks that the allowNegative option lets an order that is already charged go through anyway. It also checks that the ledger is append-only and that each row's before and after amounts match its delta.
The suite also checks that the variant total only adds up active locations. And it checks that the default-location helper returns the active location with the highest priority, and only creates one when none exists.
A race condition is the clearest case of something a stub can't test. The test checks what two real transactions do to one real row.
price-resolution.test.ts
This suite proves that a variant gets the right price for a channel and currency, including when it has to fall back to another price.
It also pins down the rules the money code must always follow. A percent coupon on top of sale prices is calculated on the discounted total. Litestore checks minOrderAmount against the original subtotal. An amount coupon never gives more than its maximum discount. Litestore rejects a coupon that is used up, expired, not yet started or deactivated.
An unknown coupon code returns a message instead of throwing. Shoppers mistype coupon codes all the time, and that isn't an error.
Mocked database
A mocked Prisma client will accept a row that Postgres would reject. So a stubbed test of a database guarantee only proves that the stub is consistent with itself:
test
↓
mocked client
↓
accepts the invalid row
↓
assertion passes
real database
↓
never consultedWhat you need to test is the database rejecting the write. A test that never reaches the database is testing something else.
Unit tests
bun run testThis runs vitest run, which runs the unit tests once. bun run verify and CI run the same command.
The unit lane points server-only at test/server-only-stub.ts, so tests can import server modules. It also leaves out **/.claude/**.
The integration config uses the same aliases for the same reason. In both lanes, the tests run the real server code.
Contract tests are not implementation tests
If a test fails because a guarantee changed, the failure reports that the guarantee changed. Deleting the test to make the build pass removes the warning, not the problem.
Test placement
Ask yourself:
Could a mocked client pass this test while production is broken?
Related
Overview
Litestore has unit, integration and end-to-end tests, and each kind runs with its own command.
Guarantees
Postgres enforces some guarantees, application code enforces others, and Litestore doesn't make the rest.
Continuous integration
CI runs a set of checks on every push, and the integration tests aren't among them.