The backend
Your project arrives with a fully configured testing environment for backend testing. The generator wrote all the files needed for running API tests:The dashboard
This is where most of your own code lives, so it is where most of your tests belong. Vitest and Playwright come configured, so there is nothing to set up:What is worth a unit test
Not components. Rendering a card to assert it shows a brand name tests React, not your feature, and breaks every time the markup moves. Test the logic between the UI and the API — the parts with decisions in them. Query keys are the clearest example. Every key must be scoped to the current store, or one store’s rows leak into another after a switch. That is a rule a test can hold you to:apps/dashboard/src/brands/client.test.ts
QueryClient means the assertion goes through TanStack’s own prefix matching rather than a hand-rolled key comparison — so it stays true if the matching rules change.
The same applies to anything else with a decision in it: a function mapping form values to an API payload, a permission predicate deciding whether a nav entry shows, a filter translating table state into Ransack params. Each is a pure function, each is a two-line test.
What is not worth a unit test
- Components. Asserting a card renders text is testing React.
BrandsClient.listitself. It is a one-line wrapper aroundadminClient.request. A test would mock the thing it wraps and assert the mock was called — proving nothing about whether the endpoint exists.- Anything the compiler already catches. A wrong prop type is a build failure, not a test case.
/brands actually return brands” cannot be answered by a unit test at all. It needs the real API, which is the next section.
End to end, through the browser
Everything above tests a layer in isolation. One Playwright spec proves the layers are actually connected — a brand created through the Admin API reaches the dashboard screen you built:apps/dashboard/e2e/brands.spec.ts
page.goto('/brands') keeps the spec honest about the real path, which is store-scoped (/:storeId/brands) and would otherwise have to be hardcoded.
Once you add a create form, replace the API call with the form itself — filling it and asserting the new row is the stronger test, because it covers the write path too.
Two conventions matter here:
- Drive the UI, assert on the UI. Fill labels, click buttons, check visible text. Avoid waiting on API responses —
expect(...).toBeVisible()polls until the condition holds, which covers nearly every case and keeps the test from breaking when the API shape changes. - Suffix names with
Date.now(). Specs share a database, and a fixed name collides with whatever an earlier run left behind.
What to run before you push
Run what you changed, not everything:You’re done
You have built one feature through every layer of Spree:- A model with a Store API and an Admin API, generated in one command
- A dashboard screen giving staff a place to manage it
- Storefront pages rendering it through typed SDK calls
- Lifecycle events other systems react to
- Tests at each layer, plus one pass across all of them

