How to Write Tests

This guide shows you how to write and run tests in the Ledger Wallet monorepo. It covers integration tests, component tests, API mocking, debugging, and common patterns.

⚠️

When you fix an issue, always provide a test to ensure the same error does not occur again.

Testing requirements

ScenarioRequirement
New features in features/ or mvvm/Must include unit and integration tests
Bug fixes in new architectureMust include a regression test covering the use case
New components in legacy codeUse new arch patterns to ease future migration
Major feature reworks in legacy codeConsider full migration to new architecture with tests (coordinate with Product)

How to write an integration test

For features with many components, integration tests provide better coverage than unit tests. Use the custom renderer to avoid manual provider setup.

// __integrations__/searchACoin.integration.test.tsx
import * as React from "react";
import {
  screen,
  waitForElementToBeRemoved,
} from "@testing-library/react-native";
import { render } from "@tests/test-renderer";
import { MarketPages } from "./shared";
import { State } from "~/reducers/types";
 
describe("Market integration test", () => {
  it("Should search for a coin and navigate to detail page", async () => {
    const { user } = render(<MarketPages />, {
      overrideInitialState: (state: State) => ({
        ...state,
        settings: {
          ...state.settings,
          featureFlags: { llmMarketNewArch: { enabled: true } },
        },
      }),
    });
 
    expect(await screen.findByText("Bitcoin (BTC)")).toBeOnTheScreen();
    expect(await screen.findByText("Ethereum (ETH)")).toBeOnTheScreen();
 
    const searchInput = await screen.findByTestId("search-box");
    await user.type(searchInput, "BTC");
 
    await waitForElementToBeRemoved(() => screen.queryByText("Ethereum (ETH)"));
 
    expect(await screen.findByText("Bitcoin (BTC)")).toBeOnTheScreen();
    await user.press(screen.getByText("Bitcoin (BTC)"));
 
    expect(await screen.findByText("Price Statistics")).toBeOnTheScreen();
  });
});

How to write a component test

For generic components used across the application:

import Button from "./Button";
 
describe("Button component", () => {
  it("should be disabled", () => {
    render(
      <Button disabled type="submit" name="hello" className="mySuperClass">
        Hello
      </Button>
    );
    expect(screen.getByRole("button")).toBeDisabled();
  });
 
  it("should dispatch onClick event", () => {
    const mockFn = jest.fn();
    render(<Button onClick={mockFn}>Hello</Button>);
 
    userEvent.click(screen.getByRole("button"));
 
    expect(mockFn).toHaveBeenCalledTimes(1);
  });
});

How to mock APIs with MSW

Use Mock Service Worker (MSW) to mock API calls. Place mock data in __mocks__/api/* and register handlers in __tests__/handlers.

Set up shared handlers

// __tests__/handlers/market.ts
import { http, HttpResponse } from "msw";
import marketsMock from "@mocks/api/market/markets.json";
import supportedVsCurrenciesMock from "@mocks/api/market/supportedVsCurrencies.json";
 
const handlers = [
  http.get(
    "https://proxycg.api.live.ledger.com/api/v3/coins/:coin/market_chart",
    ({ params }) => {
      return HttpResponse.json(
        marketsMock.find(({ id }) => id === params.coin)
      );
    }
  ),
  http.get(
    "https://proxycg.api.live.ledger.com/api/v3/simple/supported_vs_currencies",
    () => {
      return HttpResponse.json(supportedVsCurrenciesMock);
    }
  ),
];
 
export default handlers;

Override handlers for specific tests

For edge cases where the default response is not appropriate:

import { server, http, HttpResponse } from "@tests/server";
 
describe("Market page", () => {
  it("should display coins from API", async () => {
    server.use(
      http.get("https://proxycg.api.live.ledger.com/api/v3/coins/markets", () =>
        HttpResponse.json({
          data: [{ name: "Bitcoin (BTC)" }, { name: "Ethereum (ETH)" }],
        })
      )
    );
 
    const { user } = render(<MarketPages />);
 
    expect(await screen.findByText("Bitcoin (BTC)")).toBeOnTheScreen();
    expect(await screen.findByText("Ethereum (ETH)")).toBeOnTheScreen();
  });
});

Handle unhandled MSW requests

If your MSW handler is set up but the mock response is not used, the request may not have been intercepted because the test was stopped by Jest before the request was sent.

Make sure you use waitFor to assert the UI reflects the API response before the test ends:

import { render, screen, waitFor } from "@tests/test-renderer";
 
render(<MyComponent />);
 
// Wait for the API response to be reflected in the UI
await waitFor(() => {
  expect(screen.getByText("Data from API")).toBeOnTheScreen();
});
⚠️

Always ensure your test waits for async operations to complete before assertions. This prevents race conditions where MSW is closed before the request is made.


How to debug a failing test

Use screen.debug()

import { screen } from "@testing-library/react-native";
 
screen.debug();
 
// Debug a specific element
screen.debug(screen.getByText("test"));
 
// Increase the output limit (default is truncated)
screen.debug(undefined, 3000000);

Resources:

To see all ARIA roles in your rendered component (useful for finding the right ByRole query):

import { logRoles } from "@testing-library/dom";
 
const { container } = render(<MyComponent />);
logRoles(container);

How to test element absence

Use queryBy... instead of getBy... when testing for non-existence.

⚠️

getBy... throws an error when the element doesn’t exist. Use queryBy... instead, which returns null if no element is found.

// ✅ Correct - queryBy returns null if not found
expect(screen.queryByRole("link", { name: /btc/i })).not.toBeInTheDocument();
 
// ❌ Wrong - getBy throws an error if not found
expect(screen.getByRole("link", { name: /btc/i })).not.toBeInTheDocument();

How to force an initial state

You can customize the Redux state for your render call using overrideInitialState:

const { user } = render(<MarketPages />, {
  overrideInitialState: (state: State) => ({
    ...state,
    settings: {
      ...state.settings,
      supportedCounterValues: SUPPORTED_CURRENCIES,
      featureFlags: { llmMarketNewArch: { enabled: true } },
    },
  }),
});

Current Limitation: Due to redux-actions, you need to spread the existing state when overriding specific parts. This prevents accidentally replacing the initial state of reducers.


How to mock modules and functions

When a component calls a fetch or custom method that you don’t need in your test, mock the module:

jest.mock("./myModule", () => ({
  myFunction: jest.fn(() => "mocked value"),
}));

Resources:


How to run coverage

To generate a coverage report for Ledger Live Mobile:

pnpm mobile test:jest:coverage

The HTML report is available at:

apps/ledger-live-mobile/coverage/lcov-report/index.html

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.