Mocking
Three questions come up in almost every React Native suite: which mocking tool to reach for, what vi.mock actually reaches, and what to do about fake timers. This page answers each once; other pages link here.
Which tool to use
| You want to replace… | Use |
|---|---|
A native module (NativeModules.X, TurboModuleRegistry.get('X')) | mockNativeModule, under both engines |
One export of React Native (Alert, Linking, …) | vi.mock('react-native', …) with importOriginal() |
| A library that has a preset | Nothing: the preset is applied automatically. To change what it returns, see overriding a preset |
| Any other library, or your own module | vi.mock('the-module', …), as in any Vitest project; what it reaches |
Existing jest.mock(...) calls | The jest-compat layer, which hoists and applies them unchanged |
React Native itself
vi.mock('react-native') works under both engines, including the importOriginal() form that keeps everything you don't name:
vi.mock('react-native', async (importOriginal) => ({
...(await importOriginal<typeof import('react-native')>()),
Alert: { alert: vi.fn() },
}))Under the native engine this does not swap React Native for a mock: React Native still runs for real in Node, and your test and app code get a module that re-exports that same instance with your replacements on top. Platform.OS is still real, <View> still renders RCTView, and only the exports you replaced are yours.
What vi.mock reaches
Under the mock engine, React Native is a set of modules Vite serves, so vi.mock works as in any Vitest project.
Under the native engine, React Native and third-party React Native packages are loaded by Node rather than Vite. That decides what a vi.mock call can see:
A vi.mock of… | Seen by your test and app code | Seen inside a third-party package |
|---|---|---|
| Your own module | Yes | — |
react-native | Yes | No: a library's own require('react-native') gets the real one |
| A third-party package | Yes | No: another library's own require of it gets the real one |
So vi.mock('rn-library') replaces what your code imports from rn-library, and a library that depends on rn-library keeps the real one. The package's own test, tests-native/ecosystem-inline.test.tsx, checks this for react-native. Where the code under test reaches a module only through a library, mock below it: a preset, a native module, or the test on the mock engine.
Overriding a preset
A preset replaces its library for your code and for other libraries alike. To change what it returns, vi.mock the module in a setup file and spread the preset's mock; importOriginal() returns the preset's module:
// vitest.setup.ts
vi.mock('@react-navigation/native', async (importOriginal) => ({
...(await importOriginal()),
useRoute: () => ({ key: 'r', name: 'Screen', params: { id: '1' } }),
}))A vi.mock override changes what your test and app code import; libraries still get the preset's own mock. To change the preset for libraries too, use extendPresetMock, which edits the mock they all share. To run the real library instead, turn the preset off: reactNative({ presets: { navigation: false } }).
Packages that ship untranspiled source
Most of the React Native ecosystem publishes JSX, Flow or TypeScript and leaves compiling it to Metro. Under the native engine:
Detected automatically. Packages your project depends on that declare
react-nativeas a dependency or peer dependency, and their runtime dependencies, are compiled with your project's React Native Babel preset. Nothing to configure, and notransformIgnorePatterns-style list to grow.transform.includefor the rest. A package the detection misses (one that doesn't declarereact-native) fails with a parse error naming its file. List it, and its own dependencies are compiled too:tsreactNative({ transform: ['some-untranspiled-lib'] }) // the same, in the object form: reactNative({ transform: { include: ['some-untranspiled-lib'] } })transform.excludewhen the detection guesses wrong. A package listed here is never compiled, whatever selected it: detection, another package's dependencies, ortransform.include. Use it when compiling a package fails, typically a build tool that loads Babel itself:tsreactNative({ transform: { exclude: ['some-build-tool'] } })
To see what was selected, run with reactNative({ diagnostics: true }): the ownership: Node / native transform line lists the packages compiled besides React Native.
Packages a preset replaces are never compiled (their real source never loads), and neither is the test infrastructure (@testing-library/react-native and the renderers).
Fake timers
React Native Testing Library's waitFor, findBy* and userEvent delays advance fake timers only through a jest they can see.
- Native engine: vitest-native gives RNTL a
jestbacked byvi, sovi.useFakeTimers()works withwaitFor,findBy*anduserEventas it does under Jest. Nothing to configure. - Mock engine: RNTL sees no
jestunless you use the jest-compat layer. Without it,waitForandfindBy*wait in real time while the fake clock stands still, and time out. Add jest-compat, or advance the clock yourself before querying:await vi.advanceTimersByTimeAsync(ms), oruserEvent.setup({ advanceTimers: vi.advanceTimersByTime })for user events.
Two more details:
- Under the native engine, and under the mock engine with jest-compat: when RNTL loads while fake timers are on (a setup file turned them on), it switches to real timers and back. Fake timers come back with the project's
test.fakeTimersconfig; per-call options, the system time and pending timers are lost, as under Jest. Set options intest.fakeTimersor in abeforeEach. jest.useFakeTimers(options)under jest-compat takes Jest's options; see fake timers in jest-compat.
The hot runtime has two more differences around fake timers.
Next: Test Helpers.