Testing Strategy
This page explains the principles behind our testing approach, why we favor integration tests for UI validation, and the reasoning behind the custom test renderer.
Testing Principles
Our testing strategy is built on three core principles:
1. Focus on your components
Never test external library components. It’s not your responsibility to validate third-party behavior. Your tests should verify that your code works correctly, not that React, Redux, or any library does what its own tests already verify.
2. Favor integration tests for UI
When testing UI features with many components, integration tests provide better coverage than unit tests. An integration test exercises the full feature flow — from user interaction to state changes to UI updates — which catches issues that unit tests miss.
This applies specifically to UI testing. Pure helper functions, Redux reducers, and isolated logic still benefit from focused unit tests. The integration-first approach is for feature-level UI validation, where rendering the full feature with real providers catches more real-world bugs than testing components in isolation.
3. Tests should be maintainable
Avoid testing implementation details that may change. If a test breaks every time you refactor internals without changing behavior, the test is too tightly coupled. Use queries based on user-visible behavior (ByRole, ByText) rather than internal structure (ByTestId as a last resort).
Why a Custom Test Renderer
The Ledger Wallet apps depend on many providers: Redux, theming, i18n, feature flags, navigation, analytics, React Query, and more. Before the custom renderer existed, every test had to manually wrap components in a stack of providers:
// Before: manual provider stacking
<Provider store={mockStore}>
<FirebaseFeatureFlagsProvider getFeature={getFeature}>
<NavigationContainer>
<ThemeProvider theme={theme}>
<I18nextProvider i18n={i18n}>
<MyComponent />
</I18nextProvider>
</ThemeProvider>
</NavigationContainer>
</FirebaseFeatureFlagsProvider>
</Provider>This created several problems:
- Boilerplate in every test file — The provider stack was duplicated across hundreds of test files.
- Fragile mocks — Tests mocked
useSelector,useTheme,useTranslation, and other hooks individually. When provider implementations changed, many mocks broke simultaneously. - Incomplete test environments — Tests often ran with only a subset of providers, hiding bugs that only appeared when all providers were present.
The custom renderer solves these problems by providing a single render() call that includes all providers with sensible defaults. Tests use the real Redux store, real theming, real i18n — and only override what they need via overrideInitialState or initialState.
Key Benefits
| Benefit | Description |
|---|---|
| No provider mocking needed | Redux, themes, analytics, i18n, feature flags, routing are all pre-configured |
| No selector mocking | Use overrideInitialState or initialState to set up state |
| Feature flags ready | Access feature flags via FirebaseFeatureFlagsProvider |
| Routing included | NavigationContainer (Mobile) or MemoryRouter (Desktop) |
| User events included | Access user from render result for realistic interactions |
| Store access | Access store from render result to dispatch or read state |
| Full RTL API | All @testing-library utilities are re-exported |
Further Reading
These external resources provide additional context on the testing philosophy we follow:
- Common mistakes with React Testing Library
- Write fewer, longer tests
- Static vs Unit vs Integration vs E2E Testing
- Testing Overview - React
- Stop mocking fetch
See also
- Testing reference — Tools, coverage targets, renderer API, query priority
- How to Write Tests — Step-by-step guide for writing tests
- How to Use the Custom Renderer — Practical usage guide