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

BenefitDescription
No provider mocking neededRedux, themes, analytics, i18n, feature flags, routing are all pre-configured
No selector mockingUse overrideInitialState or initialState to set up state
Feature flags readyAccess feature flags via FirebaseFeatureFlagsProvider
Routing includedNavigationContainer (Mobile) or MemoryRouter (Desktop)
User events includedAccess user from render result for realistic interactions
Store accessAccess store from render result to dispatch or read state
Full RTL APIAll @testing-library utilities are re-exported

Further Reading

These external resources provide additional context on the testing philosophy we follow:


See also

Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Flex, Ledger Nano, Ledger Nano S, Ledger OS, Ledger Wallet, [LEDGER] (logo), [L] (logo) are trademarks owned by Ledger SAS.