Instruction file imported from greenymcgee/portfolio (
.cursor/rules/VITEST_RULES.mdc). Copyright stays with the author.
Pre-Test Checklist
Before writing any test file:
- Read vitest.setup.ts to understand available mocks and globals
- Read vitest.config.ts to understand import path resolution
- Check test/helpers/utils/ for available test utilities.
mockServerSession,mockServerSessionAsync,renderWithProviders, andgetApiUrlare crucial helpers used throughout the app. - ALWAYS check test/factories/ for relevant factories BEFORE creating test data manually
- Factories use Fishery and should ALWAYS be preferred over manually creating objects
- Import from the barrel export:
@/test/factories - Use
.associations()method for related data (e.g., authorId, etc.)
- Check test/fixtures/ for pre-built, reusable test data
- Fixtures are pre-configured objects exported as constants (e.g.,
PUBLISHED_POST,UNPUBLISHED_POST) - Import from the barrel export:
@/test/fixtures - Use fixtures when you need consistent, well-known test data across multiple tests
- Use factories when you need to customize data or generate unique instances
- Fixtures are pre-configured objects exported as constants (e.g.,
- Check test/servers/ for any relevant msw servers
- Check @greenymcgee/utility-types for types. Do not redefine PropsOf
Servers
Servers made with MSW are stored in test/servers/. There are a few
that still remain in the helpers directory, but any new servers should be added
to the servers directory.
Rules for Writing Tests
- We use Vitest instead of Jest.
- No abbreviations for any reason.
- No conditional logic in tests.
- Global imports are turned on for Vitest. Don't import things like
itordescribe. - When creating component tests, create a reusable const like this one
const PROPS: PropsOf<typeof Component> = { someProps: 'Hello' }. By default, the PROPS should be for the happy path of the component, and any props that need to be overridden in a test can be, but follow rule 18 for handler props. - Prefer
itovertest - Don't add line breaks between things like the render and the expect within tests, but respect the column width rules. In other words, don't add unnecessary line breaks in tests. Changes that Prettier makes to break lines are fine. Instead, add lines between
itanddescribeblocks. - When possible, it's best if the code flows naturally in the test instead of mocking returns from hooks, utils, etc.
- Name top-level component test describes like
describe("<Component />", () => {}). - Aim for no more than two expects per
itblock. - We're primarily testing functionality over style. We'd only want to test for styles in specific cases where a good argument could be made for why we need it.
- Keep any
beforeEach,afterEach, etc. callbacks out of describes if possible. Needing to nest these callbacks is a good indicator that an abstraction might be beneficial. - Test descriptions use "should" wording:
it("should behave this way", () => {}). - Test descriptions should be human readable. Avoid "should display internal name", and instead prefer "it should display the internal name".
- Avoid redundant tests. For example, if a test exists for ensuring a click handler gets called, and that test includes something like
screen.getByText("Click Me"), we don't need a test to ensure thatscreen.getByText("Click Me)"exists or is visible. - Avoid
mockin when naming. Instead ofconst mockOnClick = vi.fn()writeconst onClick = vi.fn(). This helps avoid confusion and keeps the name describing exactly what is happening. - Keep
vi.fn()implementations simple. Only mock out specific details if something is required. Example:vi.fn(() => vi.fn())is overboard if all we need to test is.toHaveBeenCalledWith("something"). - Don't provide new handler props when there is already one supplied in
PROPSfor the specific test we aim to test the handler in.vi.clearAllMockswill ensure that data is not persisting between tests. - Don't assume anything.
- Instead of testing for the absence of elements when testing against a
nullrender, use this:
const { container } = renderWithProviders(<Component />)
expect(container).toBeEmptyDOMElement()
- Avoid selecting anything by a className. Instead, if a
data-testidcould be added and used, do that. - Prefer to test inputs that have associated labels by clicking on the label. This will ensure that the
forattribute is associated with the input being tested. - Avoid mocking returns utils, hooks, facades, etc. Let the code flow through the test naturally if possible. We prefer scalable solutions vs manual mocks.
- Try
fireEventbeforeuserEventto avoid unnecessary async test setup. - Never ever import from the app directory into the test directory.
Factory Usage (Fishery)
We use Fishery factories to create test data. Always prefer factories over manually creating objects.
Basic Factory Usage
// ✅ CORRECT - Import from barrel export
import { postFactory } from "@/test/factories"
// ✅ CORRECT - Build with params
const post = postFactory.build({ title: faker.book.title() })
Using Associations
For related data (arrays of objects, nested objects), use the .associations() method:
// ✅ CORRECT - Use .associations() for authorId
const user = userFactory.build()
const post = postFactory.associations({ authorId: user.id }).build({ title: faker.book.title() })
// ❌ WRONG - Don't pass associations as regular params
const post = postFactory.build({ authorId: user.id, title: faker.book.title() })
Why Use Factories?
- Factories provide sensible defaults for all required fields
- Factories ensure type safety and consistency
- Factories automatically handle complex relationships (via
.associations()) - Tests remain maintainable when types change - update the factory, not every test
- Never manually create objects with all properties when a factory exists
Referencing Built Values (No Hard-Coded Strings)
Never hard-code values in tests that should come from fixtures or factories. Always reference the built object's properties. This ensures tests remain maintainable and won't break if factory defaults change.
const post = postFactory.build({ title: "Title" })
// ✅ CORRECT - Build the object first, then reference its property
expect(screen.getByText(post.title)).toBeVisible()
// ❌ WRONG - Hard-coded string duplicates what the factory provides
expect(screen.getByText("Title")).toBeVisible()
This also applies to fixtures:
// ✅ CORRECT - Reference fixture properties
expect(screen.getByText(PUBLISHED_POST.title)).toBeInTheDocument()
// ❌ WRONG - Hard-coded string that duplicates fixture data
expect(screen.getByText("Published Post")).toBeInTheDocument()
Fixtures vs Factories
Fixtures: Pre-built, Reusable Constants
Fixtures are pre-configured objects exported as named constants. Use fixtures when you need:
- Consistent data across multiple tests
- Well-known reference data (e.g.,
PUBLISHED_POST,UNPUBLISHED_POST) - Complex objects with specific relationships already set up
Factories: Dynamic, Customizable Builders
Factories generate objects with sensible defaults. Use factories when you need:
- Customized data for specific test cases
- Multiple unique instances
- To override specific properties while keeping defaults
When to Use Which?
| Scenario | Use |
|---|---|
| Need consistent "Published Post" across tests | Fixture: PUBLISHED_POST |
| Testing with well-known "Unpublished Post" | Fixture: UNPUBLISHED_POST |
| Need 5 different posts with varying data | Factory: Build 5 times with different params |
| MSW server needs consistent response data | Fixture: Use exported fixture constants |
| Component test needs one-off custom data | Factory: Build with custom params |
| Creating a new shared test fixture | Fixture: Build with factory, export as constant |