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
| Scenario | Requirement |
|---|---|
New features in features/ or mvvm/ | Must include unit and integration tests |
| Bug fixes in new architecture | Must include a regression test covering the use case |
| New components in legacy code | Use new arch patterns to ease future migration |
| Major feature reworks in legacy code | Consider 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:
- Debugging | Testing Library
- Testing Playground — Interactive tool for finding the right queries
Print all available roles
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:coverageThe HTML report is available at:
apps/ledger-live-mobile/coverage/lcov-report/index.htmlSee also
- How to Use the Custom Renderer — Render components with all providers pre-configured
- Testing reference — Tools, coverage targets, renderer API, query priority
- Testing Strategy — Why we favor integration tests for UI and our testing principles