{"id":"l-rn-5-2","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Set up Jest with the jest-expo preset, unit-test Trailhead’s pure logic, test components the way users use them with React Native Testing Library, and mock native modules at the boundary.","html":"<p>Every lesson in this course has pushed logic out of components and into small pure functions: <code dir=\"ltr\">parseDistanceKm</code>, <code dir=\"ltr\">hikesReducer</code>, <code dir=\"ltr\">validateHikeForm</code>, <code dir=\"ltr\">permissionAction</code>, <code dir=\"ltr\">trackDistanceKm</code>. That wasn’t only tidiness. On mobile, checking a change by hand means rebuilding, launching, navigating three screens deep and typing on a simulator keyboard. A test that checks the same thing in 20 milliseconds, every time you save, is the best trade you’ll make.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Setting up Jest</h2>\n<p>Expo’s preset, <code dir=\"ltr\">jest-expo</code>, configures Jest for React Native: it transforms your TypeScript and JSX, and <strong>mocks the native side of Expo modules</strong>, so tests run in Node with no device. Install it and React Native Testing Library:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> expo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> jest-expo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> jest</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @types/jest</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --dev</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> expo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> @testing-library/react-native</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --dev</span></span></code></pre></div>\n<p>Then point Jest at the preset in <code dir=\"ltr\">package.json</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">package.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"scripts\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"test\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"jest --watchAll\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  },</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"jest\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    \"preset\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"jest-expo\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Expo’s guide also lists a <code dir=\"ltr\">transformIgnorePatterns</code> entry for packages that ship untranspiled code; copy it from there when an import fails with “SyntaxError: Cannot use import statement outside a module”. Put tests in <code dir=\"ltr\">__tests__</code> folders next to the code they cover.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Start with pure logic</h2>\n<p>The parser from section 1 has many small rules. A table of cases documents them better than prose:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/lib/__tests__/parseDistance.test.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { parseDistanceKm } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../parseDistance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">test.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">each</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">([</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8.4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8,4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' 12 km'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'abc'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'1.2.3'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">],</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'parseDistanceKm(%j) is %p'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">input</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">expected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input)).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBe</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expected);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>Notice that the test cases are exactly the odd inputs you worried about when you wrote the function: the comma decimal, the trailing unit, the zero. Every bug report becomes one more row in a table like this, written before the fix, so the same bug can never quietly return.</p>\n<p>Reducers get the same treatment: build a state, dispatch an action, check the result, and check that the input wasn’t mutated. These tests are fast, stable and need no mocks, which is why they should be most of your suite.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Testing components like a user</h2>\n<p>React Native Testing Library renders components in memory and lets you query them the way people perceive them: by <strong>role</strong>, <strong>label</strong> and <strong>text</strong>. Here’s a test for the <code dir=\"ltr\">QuickLog</code> component from section 1:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/components/__tests__/QuickLog.test.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { render, screen, userEvent } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '@testing-library/react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { QuickLog } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../QuickLog'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'saves a valid distance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> user</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> userEvent.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setup</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">QuickLog</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> />);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> user.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByLabelText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Distance (km)'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8,4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> user.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">press</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Save'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }));</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(onSave).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHaveBeenCalledWith</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keeps Save disabled until the distance is valid'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> user</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> userEvent.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setup</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">QuickLog</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> />);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> user.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByLabelText</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Distance (km)'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'far'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Save'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> })).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBeDisabled</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>For <code dir=\"ltr\">getByLabelText</code> to find the input, the <code dir=\"ltr\">TextInput</code> needs <code dir=\"ltr\">accessibilityLabel=&quot;Distance (km)&quot;</code>. That’s the point: a test that queries by role and label fails when the screen isn’t accessible, so your tests double as an accessibility check. <code dir=\"ltr\">userEvent</code> simulates real interactions (focus, typing each character, press-in and press-out), and in current versions of the library both <code dir=\"ltr\">render</code> and the interactions are <strong>async</strong>, so await them. Matchers like <code dir=\"ltr\">toBeOnTheScreen()</code>, <code dir=\"ltr\">toHaveTextContent()</code> and <code dir=\"ltr\">toBeDisabled()</code> come with the library.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Testing implementation details</span></p>\n<p>Asserting that a component called <code dir=\"ltr\">setState</code> twice, or snapshotting its whole tree, ties tests to how the code is written rather than what it does. Refactor the component and the tests fail even though the app works; break the logic and a snapshot happily records the broken output. Assert on what a user can observe: text on screen, a button’s state, a callback called with the right value.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Mocking the device at the boundary</h2>\n<p><code dir=\"ltr\">jest-expo</code> stubs native modules, but stubs return nothing useful. When a component depends on a module’s answer, mock it with the answer you want to test:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Location </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-location'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mock</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'expo-location'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  getForegroundPermissionsAsync: jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  requestForegroundPermissionsAsync: jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">fn</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}));</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'offers Settings after a permanent denial'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  jest.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mocked</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(Location.getForegroundPermissionsAsync).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">mockResolvedValue</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    status: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'denied'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, granted: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, canAskAgain: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, expires: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'never'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">as</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Location</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">LocationPermissionResponse</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TrackScreen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> />);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  expect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> screen.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">findByRole</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'button'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, { name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Open Settings'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> })).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toBeOnTheScreen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p><code dir=\"ltr\">findBy…</code> queries wait for the element to appear, which suits screens that load asynchronously. Mock at the edge of your app (native modules, <code dir=\"ltr\">fetch</code>), not your own components, so the code under test is the code you ship. Screens that use Expo Router hooks can be rendered with <code dir=\"ltr\">renderRouter</code> from <code dir=\"ltr\">expo-router/testing-library</code>, which sets up an in-memory router with the routes you give it.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Where end-to-end tests fit</h2>\n<p>Unit and component tests can’t catch a broken native build, a permission prompt that never appears, or a keyboard covering the Save button. A few <strong>end-to-end</strong> tests on real builds cover that: tools such as Maestro (which EAS can run in the cloud) or Detox drive the actual app on a simulator. Keep them for the flows that would hurt most if broken, such as signing in and logging a hike, and let the fast tests cover everything else.</p>\n<p>Next: turning the tested app into builds that real phones can install.</p>\n","toc":[{"id":"sec-1","text":"Setting up Jest","level":2},{"id":"sec-2","text":"Start with pure logic","level":2},{"id":"sec-3","text":"Testing components like a user","level":2},{"id":"sec-4","text":"Mocking the device at the boundary","level":2},{"id":"sec-5","text":"Where end-to-end tests fit","level":2}],"takeaways":["The jest-expo preset configures Jest for React Native and mocks the native side of Expo modules, so tests run in Node without a device.","Pure functions such as parsers, reducers and validators give the most confidence per minute of test-writing; test them first.","React Native Testing Library renders components and lets you find elements by role, label and text, the way users and screen readers do.","In current versions of the library, <code dir=\"ltr\">render</code> and <code dir=\"ltr\">userEvent</code> interactions are async; await them.","Mock native modules like expo-location at the boundary with <code dir=\"ltr\">jest.mock</code>, and assert on what the user sees."],"further":[{"title":"Unit testing with Jest","url":"https://docs.expo.dev/develop/unit-testing/"},{"title":"React Native Testing Library","url":"https://testing-library.com/docs/react-native-testing-library/intro/"},{"title":"Jest mock functions","url":"https://jestjs.io/docs/mock-functions"}],"quiz":[{"q":"<p>Which Trailhead test gives the most confidence for the least effort?</p>","options":[{"text":"A snapshot of the whole hike log screen.","why":"Snapshots break on every harmless markup change and pass happily when the logic is wrong. People learn to update them without reading."},{"text":"An end-to-end test that taps through the app on a simulator to check that “8,4” is parsed.","why":"End-to-end tests are valuable for critical flows, but they’re slow and flaky for checking one parsing rule."},{"text":"A table of inputs and expected outputs for <code dir=\"ltr\">parseDistanceKm</code> and <code dir=\"ltr\">hikesReducer</code>.","why":"Correct. Pure functions run in milliseconds, need no mocks, and cover the rules users depend on."}],"answer":2},{"q":"<p>How should a component test find Trailhead’s Save button?</p>","options":[{"text":"<code dir=\"ltr\">screen.getByRole('button', { name: 'Save' })</code>","why":"Correct. It finds what users and screen readers see, and it fails if you forget the button role, which is a real accessibility bug."},{"text":"<code dir=\"ltr\">screen.getByTestId('save-btn-3')</code>","why":"Test ids work, but they test an implementation detail and miss accessibility problems. Use them as a last resort."},{"text":"By reading the third child of the rendered tree’s root View.","why":"That breaks whenever layout changes, even when the app still works."}],"answer":0},{"q":"<p>A component calls <code dir=\"ltr\">Location.getForegroundPermissionsAsync()</code>. In a Jest test it returns <code dir=\"ltr\">undefined</code> and the component shows a spinner forever. What’s the right fix?</p>","options":[{"text":"Run the test on a simulator so the real module is available.","why":"Jest runs in Node; there’s no device. The boundary needs a mock, not a simulator."},{"text":"Remove the permission check from the component when <code dir=\"ltr\">process.env.NODE_ENV</code> is <code dir=\"ltr\">test</code>.","why":"Test-only branches in production code mean you’re no longer testing the code you ship."},{"text":"Wrap the call in <code dir=\"ltr\">try</code>/<code dir=\"ltr\">catch</code> so the test passes.","why":"Nothing throws here; the mock just returns nothing. A <code dir=\"ltr\">catch</code> changes nothing and hides real errors."},{"text":"Mock the module with <code dir=\"ltr\">jest.mock('expo-location', …)</code> so the function resolves with a permission response you choose.","why":"Correct. You control the boundary and can test each state (granted, denied, and permanently denied)."}],"answer":3}],"exercise":{"kind":"fill","language":"tsx","template":"import { render, screen, userEvent } from '@testing-library/react-native';\nimport { HikeForm } from '../HikeForm';\n\ntest('shows an error, then saves once the distance is fixed', async () => {\n  const onSave = jest.fn();\n  const user = userEvent.{{0}}();\n  await {{1}}(<HikeForm onSave={onSave} />);\n\n  const distance = screen.getByLabelText('Distance (km)');\n  await user.type(screen.getByLabelText('Name'), 'Ridge Loop');\n  await user.type(distance, 'far');\n  await user.{{2}}(screen.getByRole('{{3}}', { name: 'Save' }));\n\n  expect(screen.getByText('Enter a distance in km, like 8.4.')).{{4}}();\n  expect(onSave).not.toHaveBeenCalled();\n\n  await user.clear(distance);\n  await user.type(distance, '8,4');\n  await user.press(screen.getByRole('button', { name: 'Save' }));\n\n  expect(onSave).{{5}}(expect.objectContaining({ name: 'Ridge Loop', distanceKm: 8.4 }));\n});","blanks":[{"answers":["setup"]},{"answers":["render"]},{"answers":["press"]},{"answers":["button"]},{"answers":["toBeOnTheScreen","toBeVisible"]},{"answers":["toHaveBeenCalledWith","toHaveBeenLastCalledWith"]}],"title":"Complete the form test","prompt":"<p>Fill in the blanks in this React Native Testing Library test for Trailhead’s <code dir=\"ltr\">HikeForm</code>. It should set up user events, render the form, press the Save button found <strong>by its role</strong>, check that the distance error appears on screen, and finally check that <code dir=\"ltr\">onSave</code> received the right hike.</p>\n","hints":["<code dir=\"ltr\">userEvent</code> needs a one-time call that returns the <code dir=\"ltr\">user</code> object, and the component is mounted with the library’s main function.","Users press buttons; the query looks for the accessibility role a Pressable gets from <code dir=\"ltr\">accessibilityRole</code>.","One matcher checks an element is on the screen; the other checks a mock function received specific arguments."],"explanation":"<p><code dir=\"ltr\">userEvent.setup()</code> creates a user that types and presses like a real person, and <code dir=\"ltr\">await render(...)</code> mounts the form (both are async in current versions of the library). <code dir=\"ltr\">getByRole('button', { name: 'Save' })</code> finds the button the way a screen reader does, so the test also guards accessibility. <code dir=\"ltr\">toBeOnTheScreen()</code> asserts the validation message rendered, and <code dir=\"ltr\">toHaveBeenCalledWith(expect.objectContaining(...))</code> checks the saved hike without depending on fields the test doesn’t care about, such as the generated id.</p>\n","checkLabels":{}},"words":652,"readMinutes":3}