Testing Standards & Strategy
This guide covers the testing strategy, frameworks, and best practices for Waldur HomePort.
1. The Testing Pyramid
We follow the testing pyramid model to balance speed, cost, and confidence:
- Unit & Component Tests (Base): ~70% of tests. Fast, isolated, and focused on individual functions, hooks, and components.
- Integration Tests (Middle): ~20% of tests. Focused on interactions between multiple components or complex state management (e.g., full forms, complex tables).
- E2E & Visual Tests (Top): ~10% of tests. Critical user journeys and visual consistency across browsers and themes.
2. Unit & Component Testing (Vitest + RTL)
Unit tests are used for testing React components and utilities. They are written in .test.ts or .test.tsx files located next to the code they test.
Standards
- Runner: Vitest
- Utilities:
- @testing-library/react
- @testing-library/user-event
- Goal: Verify behavior, not implementation. Use semantic queries like
screen.getByRoleorscreen.getByLabelTextover test IDs or DOM-based queries (querySelector) whenever possible. - Accessibility: Integrate automated accessibility checks using
axe-corewithin RTL tests where appropriate. - Isolation: Reset mocks between tests to prevent state leakage.
Test Harness and Providers
Avoid manual QueryClient setup. Use the renderWithProviders utility from @/test/harness to wrap components in necessary providers (like QueryClientProvider).
1 2 3 4 5 6 7 | |
If you need to inspect or spy on the queryClient (e.g., for invalidations):
1 2 3 4 5 6 7 8 9 10 | |
Quick Start Template
Use this template as a starting point for testing React components, especially Dialogs and Forms.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
Key Best Practices
- Use
userEvent: PreferuserEvent.click()overfireEvent.click()as it more accurately simulates user interaction. - Role-based Queries: Use
getByRole,getByLabelText, etc., to ensure components are accessible and tests are robust. - Wait for Changes: Use
waitFororfindBy*queries when dealing with async updates or state changes. - Clear Mocks: Always call
vi.clearAllMocks()inbeforeEachto prevent test interference. - Mock Minimal: Only mock the modules that are actually imported and used by the component under test.
3. Mocking Patterns
Mocking is essential for isolating the component under test by replacing complex dependencies (like API clients, global state, or third-party libraries) with controlled substitutes.
Rationale
In Waldur HomePort, we aim to:
- Reduce Boilerplate: Centralize common mocks to avoid repetitive setup in every test file.
- Improve Type Safety: Use
vi.mocked()for better TypeScript support when interacting with mocks. - Ensure Isolation: Reset mocks between tests to prevent state leakage.
- Simulate Real Scenarios: Provide realistic mock responses that mimic the behavior of the actual dependencies.
Centralized Mocks
Most common service dependencies are mocked globally in test/setupTests.js. This ensures they are consistently available and pre-configured.
waldur-js-client: Automatically mocked to prevent actual API calls.@/core/config: Provides a defaultENVobject.@/store/notify: ProvidesuseNotifyfor toast notifications.@/modal/actions: ProvidesuseModalwith a defaultconfirmthat resolves successfully.@/workspace/hooks: Mocks hooks likeuseUser,useCustomer, anduseProject.@/router&@uirouter/react: Globally mocked. Provides common hooks (useRouter,useCurrentStateAndParams).@phosphor-icons/react: Globally mocked using a Proxy. Icons are replaced with<span>elements containingdata-testid="IconName".@/i18n: Mockstranslateand JSX formatters to return literal strings or simple fragments.
Recommended Patterns
1. Using vi.mocked() for Type Safety
Always wrap mocked functions in vi.mocked() to get proper autocompletion and type checking for Vitest mock methods.
1 2 3 4 5 6 | |
2. Mocking List Responses
When mocking SDK list methods, use the mockListResponse utility from @/test/utils. This helper ensures the x-result-count header is correctly set, which is required by many internal utilities like fetchResultCount.
1 2 3 4 5 6 7 8 | |
3. Overriding Global Mocks for Specific Tests
If a global mock needs a specific behavior for one test case, use .mockResolvedValueOnce() or similar.
1 2 3 4 5 6 | |
4. Clearing Mocks
Ensure vi.clearAllMocks() is called in beforeEach to start every test with a clean slate.
1 2 3 | |
Global Mock Examples
API Client (waldur-js-client)
The entire client is mocked by default. Use vi.mocked() to provide specific responses.
1 2 3 4 5 6 7 8 9 10 | |
Modals (useModal)
The confirm method resolves successfully by default.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Notifications (useNotify)
Assertions on toast notifications are common. Use vi.mocked() on the methods of useNotify().
1 2 3 4 5 6 7 8 9 | |
Workspace Hooks (@/workspace/hooks)
These are pre-configured to return empty objects by default.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Routing (@uirouter/react)
Routing is globally mocked.
Asserting on Navigation
Import router from @/router to assert on navigation calls:
1 2 3 4 5 6 7 8 | |
Mocking State or Parameters
Use vi.mocked() on hooks from @uirouter/react to simulate being on a specific page or having specific URL params:
1 2 3 4 5 6 7 8 | |
Using a Real Router
If your test requires a real router instance (e.g. for complex state transition logic), use createTestRouter from @/test/router:
1 2 3 4 5 6 7 8 9 | |
Application Configuration (@/core/config)
The ENV object is globally mocked. You can safely modify ENV at the top level of your test file without affecting other files.
1 2 3 4 | |
Icons (@phosphor-icons/react)
Icons are mocked globally using a Proxy. This keeps screen.debug() clean and allows for easy assertions on icons.
1 2 3 4 5 6 7 | |
❌ Antipatterns
- ❌ Manual
@tanstack/react-queryMocking: Avoid manually mockinguseQueryClientor creating anew QueryClient()inside individual tests. UserenderWithProviders. - ❌ Inline Mock Definitions: Avoid defining mock data or functions directly in the test file's top level if they belong in a fixture.
- ❌ Deep Mocking of Internal Implementation: Avoid mocking internal component state or private methods. Mock external dependencies and assert on observable behavior.
- ❌ Hardcoded Wait Times: Avoid using
setTimeoutor fixed delays. UsewaitFor()from RTL.
Summary Table
| Dependency | How to Mock / Use |
|---|---|
| API Calls | vi.mocked(apiFunction).mockResolvedValue(...) |
| Notifications | expect(useNotify().showSuccess).toHaveBeenCalled() |
| Modals | vi.mocked(useModal().confirm).mockResolvedValue(true) |
| React Query | Use renderWithProviders |
| Routing | Handled globally; import router from @/router or hooks from @uirouter/react and use vi.mocked() |
4. Testing Form Controls
Semantic Queries
Prefer screen.getByLabelText() for form fields. Our withFormGroup HOC ensures that labels are correctly associated with their respective inputs.
React-Select Controls
Standard RTL queries often fail with react-select due to its complex internal DOM structure. Use the helpers from @/test/select to interact with these components.
Common Helpers
openAndSelectOption(user, labelText, optionText): Opens a dropdown by its label and picks an option.typeAndSelectOption(user, labelText, searchText, optionText): Useful for async selects.clearSelect(user, labelText): Clears the current selection.
1 2 3 4 5 6 7 8 9 | |
5. E2E & Visual Testing (Playwright)
E2E and visual regression tests are implemented using Playwright.
- Location:
e2e/(Workflows) ande2e-visual/(Visual Regression). - Focus: Critical user journeys and visual consistency across themes.
6. Essential Commands
| Command | Description |
|---|---|
yarn test |
Run unit tests with Vitest |
yarn test:e2e |
Run E2E tests with Playwright |
yarn test:visual |
Run visual regression tests |