<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Warren de Leon</title>
    <description>The latest articles on DEV Community by Warren de Leon (@warrendeleon).</description>
    <link>https://dev.to/warrendeleon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3849498%2F78ff4b91-2b34-4678-85ae-19aaf1642e95.png</url>
      <title>DEV Community: Warren de Leon</title>
      <link>https://dev.to/warrendeleon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/warrendeleon"/>
    <language>en</language>
    <item>
      <title>Setting up MSW v2 in React Native</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 04 May 2026 12:37:29 +0000</pubDate>
      <link>https://dev.to/warrendeleon/setting-up-msw-v2-in-react-native-2man</link>
      <guid>https://dev.to/warrendeleon/setting-up-msw-v2-in-react-native-2man</guid>
      <description>&lt;h2&gt;
  
  
  Why MSW over manual mocks
&lt;/h2&gt;

&lt;p&gt;Most React Native projects mock their API layer with &lt;code&gt;jest.fn()&lt;/code&gt;. You mock &lt;code&gt;fetch&lt;/code&gt; or your Axios instance, define what it returns, and test against that.&lt;/p&gt;

&lt;p&gt;It works. Until it doesn't.&lt;/p&gt;

&lt;p&gt;The problem: you're testing your code's interaction with a mock, not with an HTTP layer. If your API client changes how it constructs URLs, adds headers, or handles retries, the mock doesn't catch the regression. This matters even more if you're validating responses at runtime with something like Zod, because you want the validation layer to run against real response shapes, not hand-crafted mock objects. The mock always returns what you told it to return, regardless of what the code actually sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mock Service Worker (MSW)&lt;/strong&gt; intercepts requests at the network level. Your code makes real HTTP calls. MSW catches them before they leave the process and returns your mock responses. Everything between your component and the network is exercised: the Redux thunk, the Axios interceptors, the error handling, the response parsing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The key difference:&lt;/strong&gt; manual mocks replace your code. MSW replaces the network. Your code runs exactly as it would in production, up to the point where the request would leave the device.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;p&gt;The setup below was written against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Native 0.74+ with the default &lt;code&gt;react-native&lt;/code&gt; Jest preset&lt;/li&gt;
&lt;li&gt;TypeScript with the standard RN Babel config&lt;/li&gt;
&lt;li&gt;Redux Toolkit (the custom render wrapper assumes this)&lt;/li&gt;
&lt;li&gt;Node 18 or later (Node 20 recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're on an older RN version, an Expo Jest preset, or no Redux, the &lt;em&gt;concepts&lt;/em&gt; still apply but a few snippets will need adjustment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;MSW v2 runs in Jest tests via the Node.js server. The browser service worker isn't relevant for mobile, so ignore everything in the MSW docs about service-worker registration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add &lt;span class="nt"&gt;-D&lt;/span&gt; msw node-fetch@2 web-streams-polyfill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;msw&lt;/code&gt; is the obvious one. &lt;code&gt;node-fetch&lt;/code&gt; and &lt;code&gt;web-streams-polyfill&lt;/code&gt; are the polyfills MSW v2 needs in the React Native Jest environment, which I'll wire up in the next step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why pin &lt;code&gt;node-fetch@2&lt;/code&gt;?&lt;/strong&gt; &lt;code&gt;node-fetch&lt;/code&gt; v3+ is ESM-only and won't load through &lt;code&gt;require()&lt;/code&gt; in a CommonJS Jest setup file. Either pin to v2 (what this post does), or migrate the polyfills file to ESM. v2 is the lower-friction path on a default React Native Jest preset.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;Don't trust posts that say "no polyfills required".&lt;/strong&gt; MSW v2 is built on the Fetch API and Web Streams. Some Node + Jest combinations have these globals; the React Native Jest preset doesn't. Without the polyfills you'll see &lt;code&gt;ReferenceError: Response is not defined&lt;/code&gt; or &lt;code&gt;TextEncoder is not defined&lt;/code&gt; the first time MSW tries to construct a response.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Polyfills
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;jest.polyfills.cjs&lt;/code&gt; at the project root. It must be &lt;code&gt;.cjs&lt;/code&gt; (not &lt;code&gt;.ts&lt;/code&gt;) because Jest loads it before the TypeScript transformer is set up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * MSW polyfills for React Native.
 * Required for Mock Service Worker v2 in Jest tests.
 */&lt;/span&gt;

&lt;span class="c1"&gt;// TextEncoder / TextDecoder&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TextDecoder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;util&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TextEncoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TextDecoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Fetch API&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ReadableStream (for response streaming)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReadableStream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;web-streams-polyfill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReadableStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// web-streams-polyfill is optional for older MSW v2&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file runs &lt;em&gt;before&lt;/em&gt; the test framework loads, so &lt;code&gt;beforeAll&lt;/code&gt;, &lt;code&gt;jest&lt;/code&gt;, etc. aren't available here. It's purely for setting up globals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jest config
&lt;/h2&gt;

&lt;p&gt;Wire the polyfills file and a separate setup file into &lt;code&gt;jest.config.cjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;testEnvironment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;setupFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;rootDir&amp;gt;/jest.polyfills.cjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;setupFilesAfterEnv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;rootDir&amp;gt;/jest.setup.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;transformIgnorePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// The default RN preset ignores most of node_modules; MSW needs to be transformed.&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules/(?!(react-native|@react-native|msw|until-async)/)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;moduleFileExtensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two keys do the work:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;When it runs&lt;/th&gt;
&lt;th&gt;Use for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setupFiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before the Jest framework is installed&lt;/td&gt;
&lt;td&gt;Polyfills, global variables, anything that doesn't need &lt;code&gt;jest&lt;/code&gt;/&lt;code&gt;expect&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;setupFilesAfterEnv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After Jest framework, before each test file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;beforeAll&lt;/code&gt;/&lt;code&gt;afterEach&lt;/code&gt; hooks, MSW server lifecycle, custom matchers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;transformIgnorePatterns&lt;/code&gt; line is the other gotcha: the default RN preset skips transforming &lt;code&gt;node_modules&lt;/code&gt;, but MSW ships modern syntax that Jest can't run as-is. Add &lt;code&gt;msw|until-async&lt;/code&gt; to the allow-list or you'll see &lt;code&gt;SyntaxError: Cannot use import statement outside a module&lt;/code&gt; from inside &lt;code&gt;node_modules/msw/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/test-utils/msw/server.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setupServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./handlers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * MSW server for Jest. Started/stopped in jest.setup.ts.
 * Use `server.use(...errorHandlers)` to override per test.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setupServer&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server takes your default handlers (success responses) and intercepts matching requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring up the lifecycle
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;jest.setup.ts&lt;/code&gt; (which Jest loads via &lt;code&gt;setupFilesAfterEnv&lt;/code&gt;), start the server before tests, reset between tests, close after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/jest-native/extend-expect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/test-utils/msw/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// MSW server lifecycle&lt;/span&gt;
&lt;span class="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;onUnhandledRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetHandlers&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nf"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;beforeAll&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starts the server before any test runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;afterEach&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resets handlers to defaults between tests (so one test's overrides don't leak)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;afterAll&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shuts down the server after all tests complete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;onUnhandledRequest: 'warn'&lt;/code&gt; option logs a warning if your code makes a request no handler matches. In CI, switch this to &lt;code&gt;'error'&lt;/code&gt; so missed handlers fail the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onUnhandledRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onUnhandledRequest&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;If your tests use fake timers&lt;/strong&gt;, flush pending timers in &lt;code&gt;afterEach&lt;/code&gt; before resetting handlers. Otherwise an animation timer scheduled inside a component can fire after the next test starts and trigger spurious failures.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Writing handlers
&lt;/h2&gt;

&lt;p&gt;Each handler is a function that matches a request method and URL, and returns a response.&lt;/p&gt;

&lt;p&gt;A basic handler for a REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item One&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item Two&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;

  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items/:id`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Item &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;

  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;http.get&lt;/code&gt;, &lt;code&gt;http.post&lt;/code&gt;, etc. match the HTTP method&lt;/li&gt;
&lt;li&gt;✅ URL params (&lt;code&gt;:id&lt;/code&gt;) are extracted automatically&lt;/li&gt;
&lt;li&gt;✅ Request body is available via &lt;code&gt;request.json()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;HttpResponse.json()&lt;/code&gt; returns typed JSON responses with status codes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Separating fixtures from handlers
&lt;/h2&gt;

&lt;p&gt;Inline response objects work for a sketch. They don't work in a real codebase: the same shapes show up in handlers, in component tests, and in Storybook stories, and you don't want to maintain three copies.&lt;/p&gt;

&lt;p&gt;Pull the fixture data into its own file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/test-utils/msw/mockData.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item One&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-01T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item Two&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-01-02T00:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Warren de Leon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hi@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handlers then read from &lt;code&gt;mockData&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mockItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mockProfile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./mockData&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockItems&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/me`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockProfile&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same fixtures get reused in component tests where you bypass MSW and pass data directly. One source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handler sets for every scenario
&lt;/h2&gt;

&lt;p&gt;Default success handlers are the starting point. But real apps need to handle failures too. This is where most MSW setups stop. &lt;strong&gt;Don't stop here.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The bugs that actually reach production aren't the happy-path failures. They're the awkward ones: the 401 that comes back mid-session because a token expired five minutes ago, the 429 from a burst of refresh attempts after a brief network blip, the 422 with a different validation shape than your form expects, the 408 that should have been a retry but wasn't. None of those get caught if your error coverage is "what if the API returns 500?".&lt;/p&gt;

&lt;p&gt;I create separate handler sets for every error scenario the app needs to handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Success (default)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;apiHandlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;authHandlers&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Server errors&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorHandlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Internal server error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Unauthorized (expired token)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unauthorizedHandlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token has expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Rate limiting&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rateLimitHandlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/token`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;too_many_requests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Try again in 60 seconds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Timeout (never resolves)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeoutHandlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;408&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Offline (network failure)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offlineHandlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my project, I have &lt;strong&gt;11 handler sets&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Handler set&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;What it tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;handlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Default success responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;errorHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;Server error handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unauthorizedHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;Expired/invalid token flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;forbiddenHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;Banned/suspended accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;conflictHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;td&gt;Duplicate registration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;validationErrorHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;422&lt;/td&gt;
&lt;td&gt;Form validation errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rateLimitHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;Rate limiting with Retry-After&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;emailNotConfirmedHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;Email verification required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storageErrorHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;413/404&lt;/td&gt;
&lt;td&gt;File upload/delete errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeoutHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;408&lt;/td&gt;
&lt;td&gt;Network timeout simulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;offlineHandlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;td&gt;Complete network failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each set is exported and can be swapped in per test.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; The timeout handler uses &lt;code&gt;await new Promise(resolve =&amp;gt; setTimeout(resolve, 60000))&lt;/code&gt; to simulate a request that never completes. Your code's request timeout will fire first, testing the timeout handling path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Using handlers in tests
&lt;/h2&gt;

&lt;p&gt;The default handlers run automatically (registered in &lt;code&gt;setupServer&lt;/code&gt;). To test error scenarios, override them per test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@app/test-utils/msw/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;errorHandlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unauthorizedHandlers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@app/test-utils/msw/handlers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API error handling&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows error message on server failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;errorHandlers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Render component, trigger fetch, assert error UI&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirects to login on 401&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;unauthorizedHandlers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Render component, trigger fetch, assert redirect&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// No cleanup needed - afterEach in jest.setup resets handlers&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spread (&lt;code&gt;...errorHandlers&lt;/code&gt;) replaces matching handlers. Non-matching handlers from the default set remain active. After the test, &lt;code&gt;server.resetHandlers()&lt;/code&gt; restores the defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  The custom render wrapper
&lt;/h2&gt;

&lt;p&gt;MSW works best with a real Redux store, not a mocked one. The whole point is to test the actual integration: component → Redux thunk → HTTP request → MSW intercept → response → state update → UI update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/test-utils/renderWithProviders.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;combineReducers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;configureStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@reduxjs/toolkit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RenderOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;itemsReducer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@app/features/Items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authReducer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@app/features/Auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;combineReducers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;itemsReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RootState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createTestStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preloadedState&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RootState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;configureStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;reducer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rootReducer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;preloadedState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getDefaultMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;getDefaultMiddleware&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;serializableCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;immutableCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AppStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;createTestStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ExtendedRenderOptions&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Omit&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RenderOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wrapper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;preloadedState&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RootState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;store&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AppStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderWithProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;preloadedState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ExtendedRenderOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createdStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;createTestStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preloadedState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;createdStore&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Provider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;createdStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That covers Redux. Real apps usually need more: i18n, navigation, theming, toast/notification context. The wrapper is the right place to compose all of them. Add providers around &lt;code&gt;{children}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt; &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;createdStore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;I18nextProvider&lt;/span&gt; &lt;span class="na"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ThemeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ToastProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ToastProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ThemeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;I18nextProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a screen uses &lt;code&gt;react-navigation&lt;/code&gt;, wrap it in &lt;code&gt;NavigationContainer&lt;/code&gt; and an in-memory navigator for the test. The principle is the same: every provider that wraps your app in &lt;code&gt;App.tsx&lt;/code&gt; should wrap your component in &lt;code&gt;renderWithProviders&lt;/code&gt;. Anything you forget is a difference between test environment and runtime, and those differences are where flaky tests live.&lt;/p&gt;

&lt;p&gt;Now your tests render with a real store, dispatch real thunks, and MSW handles the network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loads and displays items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Default handlers return success response&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getByText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderWithProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ItemList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item One&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows error state on failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;errorHandlers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getByText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderWithProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ItemList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No manual mocking of dispatch, selectors, or fetch. The entire stack is real except the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inline handler overrides
&lt;/h2&gt;

&lt;p&gt;Sometimes you need a one-off response that doesn't fit any handler set. Define it inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;handles unexpected response shape&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;unexpected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shape&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Test that the code handles malformed responses gracefully&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for edge cases like malformed JSON, missing fields, or unexpected status codes that don't warrant a full handler set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the tests
&lt;/h2&gt;

&lt;p&gt;With everything wired up, a single test file run looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn jest src/features/Items/__tests__/ItemList.rntl.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS  src/features/Items/__tests__/ItemList.rntl.tsx
  ItemList
    ✓ loads and displays items (218 ms)
    ✓ shows error state on failure (94 ms)
    ✓ redirects to login on 401 (102 ms)
    ✓ surfaces rate-limit message (89 ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see a warning like &lt;code&gt;[MSW] Warning: captured a request without a matching request handler&lt;/code&gt;, that's &lt;code&gt;onUnhandledRequest: 'warn'&lt;/code&gt; doing its job. Either add a handler for the URL or fix the request your code is making.&lt;/p&gt;

&lt;p&gt;If the suite hangs and never finishes, MSW is usually waiting on a request that never resolves. Most often this is a &lt;code&gt;timeoutHandlers&lt;/code&gt; set that uses &lt;code&gt;setTimeout(..., 60000)&lt;/code&gt; while the test environment still has real timers. Switch to fake timers in that test (&lt;code&gt;jest.useFakeTimers()&lt;/code&gt; then &lt;code&gt;jest.advanceTimersByTime(...)&lt;/code&gt;) or shorten the simulated delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Handlers are matched in order.&lt;/strong&gt; If two handlers match the same request, the first one wins. When you &lt;code&gt;server.use(...overrides)&lt;/code&gt;, the overrides are prepended, so they take priority over defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;HttpResponse.error()&lt;/code&gt; simulates a network failure&lt;/strong&gt;, not an HTTP error. The request never gets a response. Use this for offline/no-network scenarios. For HTTP errors (500, 401, etc.), use &lt;code&gt;HttpResponse.json()&lt;/code&gt; with a status code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async handlers need &lt;code&gt;await&lt;/code&gt;.&lt;/strong&gt; If your handler reads the request body (&lt;code&gt;request.json()&lt;/code&gt;), the handler function must be &lt;code&gt;async&lt;/code&gt;. Forgetting this causes the handler to return &lt;code&gt;undefined&lt;/code&gt; instead of a response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unhandled requests are silent by default.&lt;/strong&gt; Always use &lt;code&gt;onUnhandledRequest: 'warn'&lt;/code&gt; (or &lt;code&gt;'error'&lt;/code&gt; in CI) to catch missing handlers. A silent unhandled request means your test passes for the wrong reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Response is not defined&lt;/code&gt; / &lt;code&gt;TextEncoder is not defined&lt;/code&gt;&lt;/strong&gt; means the polyfills file isn't loading. Check that &lt;code&gt;setupFiles: ['&amp;lt;rootDir&amp;gt;/jest.polyfills.cjs']&lt;/code&gt; is in your Jest config, that the file extension is &lt;code&gt;.cjs&lt;/code&gt; (not &lt;code&gt;.ts&lt;/code&gt;), and that the file path is correct relative to &lt;code&gt;rootDir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SyntaxError: Cannot use import statement outside a module&lt;/code&gt; from &lt;code&gt;node_modules/msw/&lt;/code&gt;&lt;/strong&gt; means MSW isn't being transformed. Add &lt;code&gt;msw|until-async&lt;/code&gt; to the allow-list inside &lt;code&gt;transformIgnorePatterns&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trailing slashes matter.&lt;/strong&gt; &lt;code&gt;http.get('/api/items')&lt;/code&gt; does not match a request to &lt;code&gt;/api/items/&lt;/code&gt;. Match exactly what your code sends, or use a path pattern (&lt;code&gt;http.get('/api/items*', ...)&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests pass locally and fail in CI.&lt;/strong&gt; Usually &lt;code&gt;onUnhandledRequest: 'error'&lt;/code&gt; catching a request you didn't realise your code was making in the CI environment (often analytics or crash reporting). Either add a handler for it or strip those calls in test mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full file structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project-root/
  jest.config.cjs           # Jest config (preset, setupFiles, setupFilesAfterEnv)
  jest.polyfills.cjs        # TextEncoder, fetch, ReadableStream globals
  jest.setup.ts             # Server lifecycle, custom matchers, global mocks
  src/
    test-utils/
      msw/
        handlers.ts         # All handler sets (success, error, 401, etc.)
        server.ts           # setupServer with default handlers
        mockData.ts         # Fixture data used by handlers
      renderWithProviders.tsx  # Custom render with real store + providers
      index.ts              # Barrel export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The barrel export (&lt;code&gt;index.ts&lt;/code&gt;) lets tests import common utilities from one place. For specific handler sets, import directly from the handlers file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;renderWithProviders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@app/test-utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;errorHandlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unauthorizedHandlers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@app/test-utils/msw/handlers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Yes. The setup is about 30 minutes. After that, every new test is simpler than the manual mock equivalent. You write &lt;code&gt;server.use(...errorHandlers)&lt;/code&gt; instead of &lt;code&gt;jest.fn().mockRejectedValue(new Error('Network error'))&lt;/code&gt;. The handlers are reusable across every test file. And you're testing real integration behaviour, not mock behaviour.&lt;/p&gt;

&lt;p&gt;The 11 handler sets in my project cover every error path the app handles. When I add a new API endpoint, I add handlers for it once, and every test that touches that endpoint gets correct mocking for free. The same handler-set approach also pairs well with E2E tests, where Detox + Cucumber drives the user flows and a separate runtime-mocking layer controls the API responses, but those are topics for later posts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If writing the next test is harder than skipping it, your test infrastructure is the problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;The code examples in this post are from &lt;a href="https://github.com/warrendeleon/rn-warrendeleon" rel="noopener noreferrer"&gt;rn-warrendeleon&lt;/a&gt;, my personal React Native project. The full MSW setup, handler sets, and custom render wrapper are all in the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>testing</category>
      <category>mocking</category>
      <category>jest</category>
    </item>
    <item>
      <title>I built an app the hiring panel will never open</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 27 Apr 2026 07:30:22 +0000</pubDate>
      <link>https://dev.to/warrendeleon/i-built-an-app-the-hiring-panel-will-never-open-c7n</link>
      <guid>https://dev.to/warrendeleon/i-built-an-app-the-hiring-panel-will-never-open-c7n</guid>
      <description>&lt;h2&gt;
  
  
  The hiring panel will never see the app
&lt;/h2&gt;

&lt;p&gt;That was the constraint I kept forgetting. I built a whole interview tool with wizards, timers, auto-save, keyboard shortcuts, colour-coded scores. The hiring panel gets &lt;em&gt;none of that&lt;/em&gt;. They get a &lt;strong&gt;7-page PDF&lt;/strong&gt; attached to an email.&lt;/p&gt;

&lt;p&gt;The app exists for one person: the interviewer, during the call. The PDF is what actually matters. It carries the scores, the notes, the strengths and growth areas, the hire/reject decision, and four appendices of detailed evidence. Everything the panel needs to make an offer or move on.&lt;/p&gt;

&lt;p&gt;Getting to that point took three attempts. And a bug that nearly cost a candidate their score.&lt;/p&gt;

&lt;h2&gt;
  
  
  The format problem
&lt;/h2&gt;

&lt;p&gt;I designed the scorecards for our React Native hiring process &lt;a href="https://dev.to/blog/how-i-designed-a-tech-test-scorecard-that-works-from-graduate-to-senior/"&gt;earlier this year&lt;/a&gt;. Three assessments: a &lt;strong&gt;100-check code review&lt;/strong&gt;, a &lt;strong&gt;walkthrough interview&lt;/strong&gt; scored 1–5, and a &lt;strong&gt;behavioural interview&lt;/strong&gt; mapped to our five values. The scoring worked. The format I was using to capture those scores during a live call did not.&lt;/p&gt;

&lt;p&gt;Picture the walkthrough interview. A candidate is sharing their screen, walking you through &lt;a href="https://dev.to/blog/how-to-write-a-take-home-tech-test-that-candidates-actually-want-to-do/"&gt;the tech test they've built&lt;/a&gt;, explaining their decisions. I need to read them a scripted question, listen to the answer, score it 1–5, write notes, check the time, then move to the next question. All on a video call where I'm trying to maintain eye contact and keep the conversation natural.&lt;/p&gt;

&lt;p&gt;Now imagine doing that in a &lt;strong&gt;markdown table in VS Code&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three attempts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Notion&lt;/strong&gt; was my first thought. I use it for everything personal. But it's not a tool we use at work. Building on a platform I'd be the only one using felt like a dead end, so I dropped it before starting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Markdown files&lt;/strong&gt; came next. One &lt;code&gt;.md&lt;/code&gt; per scorecard, with tables for scores and space for notes. The code review worked well this way. It's 100 yes/no checks you complete &lt;em&gt;after&lt;/em&gt; the interview at your own pace. But the walkthrough and behavioural scorecards needed to work &lt;em&gt;during&lt;/em&gt; the call. Finding the right row, typing a number, scrolling to the next section. All while a candidate is talking to me. The markdown was accurate but slow, and I was spending more attention on the document than on the person.&lt;/p&gt;

&lt;p&gt;The worst part came after the interview. Three separate markdown files, each with different formats. I had to manually combine them into one coherent document for the recruitment team. Every time, it took longer than I wanted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A localhost React app&lt;/strong&gt; was the third attempt. No backend, no database, no deployment. Just &lt;code&gt;npm run dev&lt;/code&gt; and a browser tab. Everything persists in &lt;code&gt;localStorage&lt;/code&gt;. The app dies when I close the tab and comes back when I open it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staying present during the call
&lt;/h2&gt;

&lt;p&gt;The whole point was to stop fighting the tool during the interview. Three things made the difference:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One question per screen.&lt;/strong&gt; The walkthrough is a wizard. Each step shows the script to read aloud (in a blue blockquote so I can find it instantly), the questions with large 1–5 buttons, and a notes field. No scrolling. No hunting for the right section. When I'm done, I press "Next" and the next group appears. For &lt;a href="https://dev.to/blog/how-to-pass-a-react-native-tech-test/"&gt;senior candidates&lt;/a&gt;, the wizard extends from 4 steps to 8 with an additional Part B on system design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyboard scoring.&lt;/strong&gt; Press 1 through 5 and the score registers immediately. No clicking, no dropdown menus, no confirmation dialogs. My eyes stay on the video call. The scoring happens in my peripheral vision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A section timer in the corner.&lt;/strong&gt; Not a countdown. Just a quiet elapsed time display. I glanced at it during the first walkthrough interview and realised I'd spent 8 minutes on a section that should take 4. Without the timer, I'd have run over and cut the last section short. The candidate would have lost the chance to answer questions that could have lifted their score.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that scored everyone the same
&lt;/h2&gt;

&lt;p&gt;Here's where the technical decisions got interesting. The app is built with &lt;strong&gt;React 19, TypeScript, Vite, and Tailwind v4&lt;/strong&gt;. No state management library. Just a custom &lt;code&gt;useLocalStorage&lt;/code&gt; hook and React Router.&lt;/p&gt;

&lt;p&gt;During testing, I scored a candidate's walkthrough. Every section. Every question. Full notes. I pressed "Next" to the summary screen and saw that &lt;strong&gt;every section had the same score&lt;/strong&gt;: whatever I'd entered on the last step.&lt;/p&gt;

&lt;p&gt;A stale closure bug. Each wizard step's &lt;code&gt;useCallback&lt;/code&gt; captured the walkthrough data from the &lt;em&gt;previous&lt;/em&gt; render. When step 3 saved, it overwrote steps 1 and 2 because it was still holding the old state. The classic React problem where state inside a callback doesn't update when you think it does.&lt;/p&gt;

&lt;p&gt;The fix was to bypass React state entirely on writes. Every mutation reads the &lt;em&gt;current&lt;/em&gt; candidate data directly from &lt;code&gt;localStorage&lt;/code&gt; instead of relying on the closure. A &lt;code&gt;freshCandidate()&lt;/code&gt; helper that hits &lt;code&gt;localStorage.getItem&lt;/code&gt; on every save operation. It's not elegant. It works every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;freshCandidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Candidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hl-ik-candidates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern repeats across three hooks: &lt;code&gt;useWalkthrough&lt;/code&gt;, &lt;code&gt;useBehavioural&lt;/code&gt;, and &lt;code&gt;useCodeReview&lt;/code&gt;. Each one reads fresh, writes fresh, and dispatches a custom event (&lt;code&gt;ls-sync&lt;/code&gt;) so other hook instances pick up the change. Twenty lines of persistence code. No Redux, no context providers, no middleware.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PDF nobody sees me build
&lt;/h2&gt;

&lt;p&gt;After the interview, I press "Print / PDF" and the browser generates a &lt;strong&gt;Candidate Assessment Report&lt;/strong&gt;. No PDF library. Just print CSS.&lt;/p&gt;

&lt;p&gt;Page 1 is the summary: a score table, the recommended level band, the hire/reject decision, and the offer level. Pages 2 and 3 show strengths and growth areas pulled from all three assessments, grouped by source. Then four appendices: code review breakdown, walkthrough scores with every question and note, behavioural scores by value, and a level bands reference table with the candidate's band highlighted in navy.&lt;/p&gt;

&lt;p&gt;That level bands table maps the combined score to one of &lt;strong&gt;12 tiers&lt;/strong&gt;: Graduate 1 through Senior 2+. The &lt;strong&gt;2+&lt;/strong&gt; tier is intentionally hard to reach. It means someone at the very top of their category, pushing into the next one. When a panel member sees "Associate 2+" on the PDF, they know immediately: strong for Associate, not quite SE. That single label carries more signal than a paragraph of explanation.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;behavioural gate&lt;/strong&gt; adds a second check. A candidate scoring below &lt;strong&gt;10/25&lt;/strong&gt; on values alignment doesn't proceed, regardless of their technical score. Between 10 and 14 triggers a panel discussion. 15 or above clears the gate. Technical skills can be taught. Values misalignment creates problems that grow over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Print CSS is its own discipline
&lt;/h2&gt;

&lt;p&gt;I wrote more CSS for &lt;code&gt;@media print&lt;/code&gt; than for screen. It deserves its own section because it's the part that surprised me the most.&lt;/p&gt;

&lt;p&gt;The navy background on the combined score box? &lt;strong&gt;Doesn't print.&lt;/strong&gt; Browsers strip background colours by default. I had to convert it to a white box with a heavy black border using &lt;code&gt;[style*="background: #002147"]&lt;/code&gt; selectors in the print stylesheet. Tailwind utility classes like &lt;code&gt;bg-white&lt;/code&gt; get targeted with attribute selectors (&lt;code&gt;[class*="bg-white"]&lt;/code&gt;) to override padding, borders, and margins for print.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;page-break-inside: avoid&lt;/code&gt; is a &lt;strong&gt;suggestion&lt;/strong&gt;, not a command. The browser will break inside an element if the alternative is a mostly-empty page. I spent an hour debugging why a strengths section split across two pages until I realised the content was simply too tall for the remaining space.&lt;/p&gt;

&lt;p&gt;Heading styles needed explicit inline &lt;code&gt;border-bottom&lt;/code&gt; because Tailwind classes get stripped or overridden by the print reset. Font sizes switch from &lt;code&gt;rem&lt;/code&gt; to &lt;code&gt;pt&lt;/code&gt;. Interactive elements (textareas, checkboxes, dropdowns) are hidden. The entire print layout lives in a separate &lt;code&gt;CandidatePrintReport&lt;/code&gt; component that renders inside &lt;code&gt;hidden print:block&lt;/code&gt;. Clean separation. The screen never sees the print layout, the print never sees the buttons.&lt;/p&gt;

&lt;p&gt;If I built this again, I'd design the print layout &lt;em&gt;first&lt;/em&gt; and the screen layout second. The PDF is the deliverable. The screen is just the input form.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd change
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tests before scoring logic.&lt;/strong&gt; Red flag deductions, stretch bonuses, level band lookups, the behavioural gate threshold. These are all pure functions now, extracted into &lt;code&gt;utils/scoring.ts&lt;/code&gt;. They're the kind of code that breaks silently when you tweak a boundary. I wrote them last. They should have been first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The markdown import parser is fragile.&lt;/strong&gt; It uses regex to read Y/N values from scored code review files. It works for the specific format I designed, but it's brittle. A different table alignment or an extra column breaks it. A proper parser with error recovery would be more resilient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility was added late.&lt;/strong&gt; WCAG AA compliance (dynamic page titles, heading hierarchy, colour contrast ratios, roving tabindex on score selectors, aria-live on save indicators) was retrofitted rather than built in. It all passes now, but it would have been cleaner to build accessible from the start. Internal tools deserve the same standards as public-facing ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real test
&lt;/h2&gt;

&lt;p&gt;I used this app for the first time in an actual interview last week. The candidate didn't know I was using anything unusual. They presented their &lt;a href="https://dev.to/blog/how-to-pass-a-react-native-tech-test/"&gt;take-home submission&lt;/a&gt;, I scored, we talked. I wasn't scrolling, I wasn't typing into markdown tables, I wasn't losing my place. After the call, I pressed one button and had the PDF ready in seconds.&lt;/p&gt;

&lt;p&gt;That's the whole point. The best interview tool is the one that &lt;strong&gt;disappears&lt;/strong&gt;. The candidate should feel like they're having a conversation, not being processed by a system. The scoring, the timers, the level calculations, the PDF generation: all of that should be invisible. If the tool is doing its job, nobody notices it's there.&lt;/p&gt;

</description>
      <category>engineeringmanagement</category>
      <category>hiring</category>
      <category>react</category>
      <category>internaltools</category>
    </item>
    <item>
      <title>How to write a take-home tech test that candidates actually want to do</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 20 Apr 2026 07:30:11 +0000</pubDate>
      <link>https://dev.to/warrendeleon/how-to-write-a-take-home-tech-test-that-candidates-actually-want-to-do-3b4j</link>
      <guid>https://dev.to/warrendeleon/how-to-write-a-take-home-tech-test-that-candidates-actually-want-to-do-3b4j</guid>
      <description>&lt;h2&gt;
  
  
  The test nobody finishes
&lt;/h2&gt;

&lt;p&gt;Most take-home tech tests fail before the candidate writes a single line of code.&lt;/p&gt;

&lt;p&gt;They clone the repo. They run &lt;code&gt;npm install&lt;/code&gt;. Something breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;45 minutes later&lt;/strong&gt;, they're debugging a Ruby version mismatch, a missing CocoaPod, or a Node version that doesn't work with the bundler. By the time the app runs, they've burnt through their patience and half their evening.&lt;/p&gt;

&lt;p&gt;The best candidates, the ones you actually want to hire, are the most likely to walk away. They have options. They'll pick the company that respects their time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚩 &lt;strong&gt;This happened to us.&lt;/strong&gt; Our first candidate spent two hours fighting Ruby version issues before writing any application code. His system Ruby was too old. He upgraded to Ruby 4, which broke the bundler. He downgraded to 3.3, but the vendored bundler was incompatible. Each step was a back-and-forth message. Two hours. Zero lines of application code written.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That experience changed how I thought about the test. The questions were fine. &lt;strong&gt;The developer experience was the problem.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat the test like a product
&lt;/h2&gt;

&lt;p&gt;This became my guiding principle. The tech test is the first real interaction a candidate has with your engineering culture. Everything they experience tells them something about you.&lt;/p&gt;

&lt;p&gt;If the setup is broken → they think your codebase is broken.&lt;br&gt;
If the brief is vague → they think your specs are vague.&lt;br&gt;
If the timeline is unrealistic → they think your deadlines are unrealistic.&lt;/p&gt;

&lt;p&gt;I started treating the test the same way I'd treat a product:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product thinking&lt;/th&gt;
&lt;th&gt;Applied to the tech test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User research&lt;/td&gt;
&lt;td&gt;What frustrates candidates about tech tests?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear requirements&lt;/td&gt;
&lt;td&gt;A detailed brief with wireframes and rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer experience&lt;/td&gt;
&lt;td&gt;Starter project, setup script, path aliases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;td&gt;Linked guides for every question they might have&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous improvement&lt;/td&gt;
&lt;td&gt;Update after every round based on what went wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After the Ruby incident, I added a setup script, pinned the Ruby version, committed a Gemfile.lock with a modern bundler, and added a troubleshooting section to the README.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The next candidate was coding in under two minutes.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup script
&lt;/h2&gt;

&lt;p&gt;The single biggest improvement: a &lt;code&gt;setup.sh&lt;/code&gt; that handles everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Checks Node version (installs via nvm if needed)&lt;/li&gt;
&lt;li&gt;✅ Checks Ruby version (supports rbenv, rvm, and asdf)&lt;/li&gt;
&lt;li&gt;✅ Checks for Xcode CLI tools and CocoaPods&lt;/li&gt;
&lt;li&gt;✅ Runs &lt;code&gt;yarn install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ Runs &lt;code&gt;bundle install&lt;/code&gt; and &lt;code&gt;pod install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ Tells you exactly what to fix if something is wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key design choice: the script &lt;strong&gt;asks before installing anything&lt;/strong&gt;. It detects what the candidate already has and works with it. A candidate using rbenv gets rbenv. A candidate using rvm gets rvm. Their environment is respected, not overwritten.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Pin your versions in the repo: &lt;code&gt;.ruby-version&lt;/code&gt;, &lt;code&gt;.nvmrc&lt;/code&gt;, &lt;code&gt;Gemfile.lock&lt;/code&gt; with a modern bundler. Then write a setup script that reads them. Every minute a candidate spends on setup is a minute they're not spending on code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The starter project
&lt;/h2&gt;

&lt;p&gt;I give candidates a fully configured project. Not a blank repo. A working app.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Included&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript in strict mode&lt;/td&gt;
&lt;td&gt;No ambiguity about language expectations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React Navigation v7 with typed params&lt;/td&gt;
&lt;td&gt;Navigation is boilerplate, not a test of skill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jest + React Native Testing Library&lt;/td&gt;
&lt;td&gt;Configured with native module mocks, ready to write tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESLint + Prettier&lt;/td&gt;
&lt;td&gt;Consistent code style from line one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path aliases (&lt;code&gt;@app/*&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;../../../&lt;/code&gt; import chains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom test render wrapper&lt;/td&gt;
&lt;td&gt;NavigationContainer included, just render and assert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three placeholder screens&lt;/td&gt;
&lt;td&gt;"Replace me", clear starting point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A passing smoke test&lt;/td&gt;
&lt;td&gt;Proof the setup works before they change anything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Everything compiles. Everything runs. The smoke test passes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm not testing whether someone can configure a bundler or debug a TypeScript path alias. I'm testing whether they can &lt;strong&gt;build application code&lt;/strong&gt;. The starter project removes every obstacle between "I cloned the repo" and "I'm writing my first component."&lt;/p&gt;

&lt;p&gt;Some candidates start from scratch anyway. That's fine. The starter is optional. But most use it, and the result is the same: instead of spending their first hour fighting config, they spend it making architectural decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The brief: clear about what, not how
&lt;/h2&gt;

&lt;p&gt;Some tech tests specify exactly how to build things: which state management library, which folder structure, which API client. That approach works when you want consistency. But for us, those decisions are the most interesting part of the submission.&lt;/p&gt;

&lt;p&gt;Our brief takes a different approach. It explains &lt;strong&gt;what&lt;/strong&gt; the app should do in detail, and says nothing about &lt;strong&gt;how&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Screen wireframes&lt;/strong&gt; show the data and interactions (ASCII layouts, not pixel designs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A requirements table&lt;/strong&gt; spells out the rules (max 6 items, add from detail, remove from list)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A technical requirements table&lt;/strong&gt; lists the non-negotiables (React Native, TypeScript, React Navigation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's deliberately missing: architecture prescriptions. The candidate chooses the state management, the folder structure, the API client, the testing strategy.&lt;/p&gt;

&lt;p&gt;A candidate who picks Redux Toolkit tells me something different from one who picks Zustand. Neither is wrong. &lt;em&gt;Both are interesting.&lt;/em&gt; And the reasoning behind the choice is what the walkthrough conversation is built on.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If your brief specifies the architecture, you're testing compliance, not engineering. The best briefs describe the &lt;em&gt;what&lt;/em&gt; in detail and leave the &lt;em&gt;how&lt;/em&gt; completely open.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Respecting people's time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Candidates get 7 days. The work should take 4 to 6 hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We say this explicitly. In the brief and in the submission guide. Twice, because people miss it the first time.&lt;/p&gt;

&lt;p&gt;7 days gives flexibility. Some people work across a weekend. Some do an hour each evening. Some block a Saturday morning. The timeline respects that candidates have jobs, families, and lives outside of interviewing.&lt;/p&gt;

&lt;p&gt;The 4-to-6-hour estimate is honest. I built the test myself to verify it. A competent React Native developer can build all three screens with state management, API integration, basic tests, and a README in that time. Some choose to invest more. That's their choice, not our expectation.&lt;/p&gt;

&lt;p&gt;If a candidate needs more time, we give it. No questions asked.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Going silent and submitting three days late with no explanation is a different signal from sending a message saying "I need a couple more days." Communication matters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tell them what you're looking for
&lt;/h2&gt;

&lt;p&gt;Early on, a candidate told us they'd spent an hour styling buttons because they assumed UI polish mattered to us. It didn't. We were looking at architecture and testing. That hour was wasted because we hadn't told them what counted.&lt;/p&gt;

&lt;p&gt;Now we're explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ How you think about architecture and code organisation
✅ How you break down a problem into components and data flows
✅ How you make and justify technical decisions
✅ How you handle edge cases and error states
✅ How well you know your own code

❌ We're NOT judging visual design or pixel-perfect UI
❌ We're NOT expecting a production-ready app in a take-home
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When candidates know we care about architecture and trade-offs more than styling, they allocate their time accordingly. &lt;strong&gt;Better signal for us. Better experience for them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We also tell them upfront that we might use AI tooling as a pre-check, but every submission is manually reviewed and scored by the hiring panel. Transparency builds trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The walkthrough is not an interrogation
&lt;/h2&gt;

&lt;p&gt;The walkthrough is a conversation. The candidate drives for the first 10 minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Demo the app&lt;/strong&gt;: walk through all screens, show the features working&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the tests&lt;/strong&gt;: show them passing live&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Walk through the code&lt;/strong&gt;: explain structure and decisions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After the presentation, we ask questions. But the framing matters. We say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Don't worry if something doesn't work as expected during the demo. That happens. If it does, just talk me through what you think went wrong and how you'd fix it. That tells me more than a perfect demo would."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't just being nice. Watching someone diagnose a bug in their own code is one of the strongest signals you can get. A candidate who says &lt;em&gt;"Oh, I think the useEffect dependency array is wrong here"&lt;/em&gt; is showing you exactly how they work.&lt;/p&gt;

&lt;p&gt;A perfect demo shows you nothing except that they rehearsed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation as a first-class feature
&lt;/h2&gt;

&lt;p&gt;The test comes with proper documentation. Not just a README. A set of linked markdown files:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Assessment Brief&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requirements, screen wireframes, party rules, technical requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Guide&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Endpoints, GraphQL vs REST options, client recommendations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Starter Project&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What's included, project structure, available commands, testing setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Submission &amp;amp; Walkthrough&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How to submit, what happens in the walkthrough, tips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stretch Goals&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optional extras and what each one demonstrates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every question a candidate might have is answered before they need to ask it. This isn't just about being helpful. It's about &lt;strong&gt;removing ambiguity as a variable&lt;/strong&gt;. I don't want to evaluate how well someone interprets a vague brief. I want to evaluate how they build software when the requirements are clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd change next time
&lt;/h2&gt;

&lt;p&gt;The test isn't perfect. What's on my list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A video walkthrough of the starter project.&lt;/strong&gt; A 3-minute Loom showing the folder structure, how to run it, and where to start. Some people learn better from video than docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;.env.example&lt;/code&gt; file.&lt;/strong&gt; Even though the test uses a public API with no keys, it sets the right pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing the setup on a clean machine.&lt;/strong&gt; I built the test on my own laptop with years of tooling. Every assumption about "everyone has this installed" was wrong. The first candidate proved it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The structure is right though. Setup script. Starter project. Clear brief. Honest timeline. Proper documentation. Transparent evaluation criteria.&lt;/p&gt;

&lt;p&gt;If you're designing a tech test and candidates keep dropping out, don't look at the questions first. Look at the developer experience. &lt;strong&gt;The best tech test is one where the candidate spends 100% of their time on the thing you're actually evaluating, and 0% on everything else.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the last in a series about building a hiring process from scratch. The earlier posts cover &lt;a href="https://warrendeleon.com/blog/why-i-redesigned-our-react-native-tech-test-in-my-first-week/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=take-home-tech-test" rel="noopener noreferrer"&gt;why I redesigned the test&lt;/a&gt;, &lt;a href="https://warrendeleon.com/blog/how-to-pass-a-react-native-tech-test/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=take-home-tech-test" rel="noopener noreferrer"&gt;advice for candidates taking one&lt;/a&gt;, and &lt;a href="https://warrendeleon.com/blog/how-i-designed-a-tech-test-scorecard-that-works-from-graduate-to-senior/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=take-home-tech-test" rel="noopener noreferrer"&gt;how the scoring works&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;We're hiring!&lt;/strong&gt; We're looking for React Native engineers to join the Mobile Platform team at Hargreaves Lansdown. &lt;a href="https://warrendeleon.com/hiring/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=take-home-tech-test" rel="noopener noreferrer"&gt;View open roles&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>engineeringmanagement</category>
      <category>hiring</category>
      <category>techinterviews</category>
      <category>devrel</category>
    </item>
    <item>
      <title>How I designed a tech test scorecard that works from Graduate to Senior</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 13 Apr 2026 07:30:10 +0000</pubDate>
      <link>https://dev.to/warrendeleon/how-i-designed-a-tech-test-scorecard-that-works-from-graduate-to-senior-97</link>
      <guid>https://dev.to/warrendeleon/how-i-designed-a-tech-test-scorecard-that-works-from-graduate-to-senior-97</guid>
      <description>&lt;h2&gt;
  
  
  The problem with "is this a 3 or a 4?"
&lt;/h2&gt;

&lt;p&gt;When I started building the hiring process for my squad, I knew I wanted a structured scorecard from day one. I wrote about the tech test itself in &lt;a href="https://dev.to/blog/why-i-redesigned-our-react-native-tech-test-in-my-first-week/"&gt;an earlier post&lt;/a&gt;. The test worked. The scoring didn't. At least, not the way I first designed it.&lt;/p&gt;

&lt;p&gt;My first scorecard used a 1–5 scale for each criterion. "TypeScript usage: score 1 to 5." "State management: score 1 to 5." Each criterion had a rubric describing what each score meant. It looked thorough on paper.&lt;/p&gt;

&lt;p&gt;Then I tried to use it.&lt;/p&gt;

&lt;p&gt;Two people reviewed the same submission. One scored the TypeScript a 3 ("types are there but not strict"). The other scored it a 4 ("clean types throughout, good use of typed hooks"). They were both looking at the same code. They just interpreted the rubric differently.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If two reasonable people can disagree on the score, the rubric isn't specific enough. The problem isn't the reviewers. It's the tool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Checklists over rubrics
&lt;/h2&gt;

&lt;p&gt;The fix was embarrassingly simple: replace every subjective score with a &lt;strong&gt;yes/no checklist&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A single criterion, before and after. This is TypeScript usage:&lt;/p&gt;

&lt;h3&gt;
  
  
  Before: subjective rubric
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Strong typing throughout, strict mode, generics where appropriate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Clean types, minimal &lt;code&gt;any&lt;/code&gt;, props and navigation typed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Types for main structures, some &lt;code&gt;any&lt;/code&gt; leakage, works but not strict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;TypeScript used poorly, frequent &lt;code&gt;any&lt;/code&gt;, adds little safety&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;any&lt;/code&gt; everywhere, effectively JavaScript with &lt;code&gt;.tsx&lt;/code&gt; extensions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The problem: "clean types" and "types for main structures" are both reasonable descriptions of the same code. One reviewer sees a 3, another sees a 4. Both are right.&lt;/p&gt;

&lt;h3&gt;
  
  
  After: observable checklist
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Source files use .ts/.tsx extensions
✅ Interfaces or types exist for API data, state shape, and component props
✅ Navigation params are typed
✅ Zero any in production code
☐  Typed hooks used (useAppSelector, useAppDispatch)
☐  Strict TypeScript enabled
☐  Zod or Yup schemas for validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same criterion. Seven checks. Each one is a fact you can verify by looking at the code. Two reviewers will tick the same boxes because there's nothing to interpret.&lt;/p&gt;

&lt;p&gt;The first four checks are baseline (any competent candidate will have these in a 4–6 hour submission). The last three are signals of deeper experience. &lt;strong&gt;The ordering does the levelling for you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I did this for every criterion across four sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core Functionality&lt;/strong&gt;: does the app work?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Layer &amp;amp; API&lt;/strong&gt;: how does it fetch and manage data?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Quality&lt;/strong&gt;: is the code well-written and well-organised?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt;: is it tested, and how?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;100 checks. 100 points. One point each.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Same test, different ceiling
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most excited about. The checks are ordered by how much investment they represent.&lt;/p&gt;

&lt;p&gt;The first few checks in each criterion are things any competent candidate will achieve in &lt;strong&gt;4–6 hours&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the FlatList render items?&lt;/li&gt;
&lt;li&gt;Does pagination work?&lt;/li&gt;
&lt;li&gt;Does the party screen have an empty state?&lt;/li&gt;
&lt;li&gt;Are there types for the main data structures?&lt;/li&gt;
&lt;li&gt;Is there at least one test file?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the baseline. If you built the thing the brief asked for, you pass these.&lt;/p&gt;

&lt;p&gt;The later checks require more time, deeper experience, or both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GraphQL instead of REST&lt;/li&gt;
&lt;li&gt;Runtime response validation with Zod&lt;/li&gt;
&lt;li&gt;MSW for HTTP mocking in tests&lt;/li&gt;
&lt;li&gt;Feature-first project structure&lt;/li&gt;
&lt;li&gt;BDD with Cucumber&lt;/li&gt;
&lt;li&gt;Coverage thresholds enforced&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't things you do in a weekend. They're patterns you've learnt from building real production apps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key insight:&lt;/strong&gt; A candidate investing 4–6 hours scores in the 50–65 range. A candidate investing a full week with years of experience might score 85–95. &lt;strong&gt;The brief is the same. The expectations scale with the score.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How the levels map
&lt;/h2&gt;

&lt;p&gt;The total score maps directly to a level:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Code review score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Graduate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20–45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Associate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;46–64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Software Engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;65–88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Senior&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;89–100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The code review score isn't the whole picture. The walkthrough call adds more signal. But the code review is the foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respecting the time constraint
&lt;/h2&gt;

&lt;p&gt;A tech test is &lt;strong&gt;not a production app&lt;/strong&gt;. Candidates have jobs, families, lives. They're giving you their evening or their weekend. Penalising someone for not implementing a caching layer or not co-locating their styles would be like marking down a timed essay for not having footnotes.&lt;/p&gt;

&lt;p&gt;That's why the baseline checks matter. Getting all of them right scores you around &lt;strong&gt;50–60 out of 100&lt;/strong&gt;. That's Associate to Software Engineer territory. On my old rubric, a "3 out of 5" &lt;em&gt;sounded&lt;/em&gt; like a consolation prize. 55 out of 100 on the checklist is a positive result with a clear path to the next level.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "above baseline" looks like
&lt;/h2&gt;

&lt;p&gt;The later checks are where candidates differentiate themselves. These aren't requirements. They're &lt;strong&gt;signals&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A candidate who adds &lt;strong&gt;Detox E2E tests&lt;/strong&gt; with extracted helpers is telling me something about their testing culture.&lt;/p&gt;

&lt;p&gt;A candidate who implements &lt;strong&gt;GraphQL with Apollo&lt;/strong&gt; is telling me something about their API thinking.&lt;/p&gt;

&lt;p&gt;A candidate who sets up &lt;strong&gt;MSW with multiple handler sets&lt;/strong&gt; (success, error, 401, timeout, offline) is telling me they've debugged production API failures before.&lt;/p&gt;

&lt;p&gt;None of these are required. &lt;strong&gt;All of them are noticed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The stretch goals sit on top of the 100 points as bonuses: search, dark mode, accessibility, i18n, feature-first structure, Storybook, ErrorBoundary. These are the marks of someone who had time and chose to invest it wisely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The walkthrough changes everything
&lt;/h2&gt;

&lt;p&gt;The code review gives me a number. The walkthrough gives me &lt;strong&gt;context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A candidate who scores 65 on the code review might jump to 85 after the walkthrough if they can articulate every trade-off, explain what they'd change with more time, and navigate their codebase from memory. The number measures what they built. The conversation measures how they think.&lt;/p&gt;

&lt;p&gt;I designed the walkthrough as a set of &lt;strong&gt;question tables&lt;/strong&gt;. Each question has five signal descriptions, from "can't find the code" to "explains it from memory with edge cases." The interviewer ticks one row per question. No more "was that walkthrough a 3 or a 4?"&lt;/p&gt;

&lt;p&gt;For Senior candidates, there's an additional &lt;strong&gt;system design section&lt;/strong&gt; in the same call. No separate interview. The last 15–20 minutes shift from "show me your code" to "how would you design this for a team of 20 engineers?" The same question tables, the same tick-one-row format.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learnt building this
&lt;/h2&gt;

&lt;p&gt;Building this scorecard taught me more about hiring design than anything I've read about it. The lessons that stuck:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with checklists, not rubrics.&lt;/strong&gt; Every time I wrote a rubric ("5 = excellent, 3 = good, 1 = poor"), it turned into a debate about what "good" means. Checklists end the debate. Either the thing exists in the code or it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order the checks by investment, not importance.&lt;/strong&gt; The first checks aren't more important than the last. They're just more achievable in 4–6 hours. A Senior candidate who skips check 3 but nails check 7 isn't penalised for the skip because the total still reflects their level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate what you can see from what you need to ask.&lt;/strong&gt; The code review scorecard is 100% observable from the code. No "is the architecture clean?" questions. The walkthrough is 100% conversational. No code-reading during the call. Each document has one job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respect the time constraint.&lt;/strong&gt; If a check would require more than 6 hours of work from a competent Software Engineer, it belongs in the upper half of the checklist, not the baseline. I kept catching myself writing baseline checks that were really Senior expectations. The question I kept asking: &lt;em&gt;"Would I expect this from someone doing this test after work on a Wednesday evening?"&lt;/em&gt; If the answer was no, it moved up.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's still evolving
&lt;/h2&gt;

&lt;p&gt;I've used this scorecard for our first round of React Native hiring. My peer EM reviewed it and adopted it for his squad's hires too. That's the test of a good system: &lt;strong&gt;someone else can pick it up and use it without you in the room.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm not pretending it's perfect. The levels might need recalibrating after more candidates go through. Some checks might turn out to be too easy or too hard. The stretch goals might need rebalancing.&lt;/p&gt;

&lt;p&gt;The structure is right though:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Checklists, not rubrics&lt;/li&gt;
&lt;li&gt;✅ Observable facts, not opinions&lt;/li&gt;
&lt;li&gt;✅ Ordered by investment&lt;/li&gt;
&lt;li&gt;✅ Same test for everyone&lt;/li&gt;
&lt;li&gt;✅ Different ceiling for different levels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building a hiring process and your interviewers keep disagreeing on scores, try replacing your rubric with a checklist. You might be surprised how much agreement you get when you stop asking &lt;em&gt;"how good is this?"&lt;/em&gt; and start asking &lt;em&gt;"is this here?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want the candidate's perspective on what this scorecard evaluates, I wrote a companion post: &lt;a href="https://dev.to/blog/how-to-pass-a-react-native-tech-test/"&gt;How to pass a React Native tech test&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The best scoring systems don't measure how you feel about the code. They measure what's in the code.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>engineeringmanagement</category>
      <category>hiring</category>
      <category>techinterviews</category>
    </item>
    <item>
      <title>How to pass a React Native tech test</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Mon, 06 Apr 2026 07:30:11 +0000</pubDate>
      <link>https://dev.to/warrendeleon/how-to-pass-a-react-native-tech-test-4642</link>
      <guid>https://dev.to/warrendeleon/how-to-pass-a-react-native-tech-test-4642</guid>
      <description>&lt;h2&gt;
  
  
  This is from the other side of the table
&lt;/h2&gt;

&lt;p&gt;I review React Native tech test submissions. I've seen what gets people hired and what gets them rejected. Most of the rejections aren't because the candidate can't code. They're because the candidate didn't show the right things.&lt;/p&gt;

&lt;p&gt;This post is the advice I'd give a friend before they submitted a take-home tech test. Not theory. Specific, practical things that move you from "maybe" to "yes."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I wrote about why I redesigned a tech test from the hiring manager's perspective in &lt;a href="https://warrendeleon.com/blog/why-i-redesigned-our-react-native-tech-test-in-my-first-week/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=pass-rn-tech-test" rel="noopener noreferrer"&gt;a separate post&lt;/a&gt;. This one is the other side: how to pass one.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the brief twice. Then read it again.
&lt;/h2&gt;

&lt;p&gt;Sounds obvious. It's the most common mistake.&lt;/p&gt;

&lt;p&gt;If the brief says "build three screens with navigation," don't build two. If it says "use TypeScript," don't use JavaScript. If it says "manage a list of up to 6 items," make sure adding a 7th is handled gracefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reviewers check requirements like a checklist.&lt;/strong&gt; Every missing requirement is points dropped. Not because we're pedantic, but because following a spec is part of the job. If you miss requirements in a tech test with a clear brief, what happens with a vague Jira ticket?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Read the brief before you start. Read it again halfway through. Read it one final time before you submit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Project structure matters more than you think
&lt;/h2&gt;

&lt;p&gt;The first thing I do when I open a submission is look at the folder structure. Before I read a single line of code, the structure tells me how you think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type-first structure&lt;/strong&gt; (screens/, components/, hooks/, services/):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  components/
  hooks/
  screens/
  services/
  types/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Feature-first structure&lt;/strong&gt; (each feature is self-contained):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  features/
    product-list/
    product-detail/
    favourites/
  shared/
    components/
    hooks/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither is wrong. But feature-first shows you've thought about how the app scales. If I ask "what happens when 5 teams work on this codebase?" and your structure already answers that question, you're ahead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚩 &lt;strong&gt;Red flag:&lt;/strong&gt; Everything in a flat &lt;code&gt;src/&lt;/code&gt; folder with no organisation. It suggests the coding started before the architecture was planned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TypeScript is not optional
&lt;/h2&gt;

&lt;p&gt;Even if the brief says "TypeScript preferred," treat it as required. Submitting plain JavaScript in 2026 is an automatic downgrade.&lt;/p&gt;

&lt;p&gt;But it's not enough to just use TypeScript. Use it &lt;em&gt;well&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Do this&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type your props&lt;/td&gt;
&lt;td&gt;Every component should have a typed props interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type your API responses&lt;/td&gt;
&lt;td&gt;Don't use &lt;code&gt;any&lt;/code&gt; for data from the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type your navigation params&lt;/td&gt;
&lt;td&gt;React Navigation has excellent TypeScript support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one &lt;code&gt;any&lt;/code&gt; I'll forgive: complex third-party library types that would take an hour to figure out. Acknowledge it in a comment. &lt;em&gt;"// TODO: type this properly — ran out of time"&lt;/em&gt; is better than pretending it doesn't exist.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚩 &lt;strong&gt;Red flag:&lt;/strong&gt; &lt;code&gt;any&lt;/code&gt; scattered throughout the codebase with no acknowledgment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  State management: pick something and own it
&lt;/h2&gt;

&lt;p&gt;I don't care whether you use Redux Toolkit, Zustand, React Context, or Jotai. I care that you picked it deliberately and can explain why.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;What it signals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Context&lt;/strong&gt; for a three-screen app&lt;/td&gt;
&lt;td&gt;Perfectly reasonable. Lightweight, no dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Redux Toolkit&lt;/strong&gt; for a three-screen app&lt;/td&gt;
&lt;td&gt;Fine, but I'll ask why. "It's what I know best" is an honest answer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Zustand&lt;/strong&gt; with a clean store&lt;/td&gt;
&lt;td&gt;Shows you're current with the ecosystem.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you go with Redux, &lt;strong&gt;use Redux Toolkit&lt;/strong&gt;. Not the old &lt;code&gt;switch/case&lt;/code&gt; reducer pattern. If I see &lt;code&gt;createStore&lt;/code&gt; instead of &lt;code&gt;configureStore&lt;/code&gt;, or manual action type constants instead of &lt;code&gt;createSlice&lt;/code&gt;, it suggests the Redux knowledge might need refreshing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ State logic separated from the UI&lt;/li&gt;
&lt;li&gt;✅ Actions, reducers, and selectors in their own files&lt;/li&gt;
&lt;li&gt;✅ Business rules (like max party size) enforced in the state layer&lt;/li&gt;
&lt;li&gt;✅ Updates are predictable&lt;/li&gt;
&lt;li&gt;❌ Business logic living inside components&lt;/li&gt;
&lt;li&gt;❌ State scattered across &lt;code&gt;useState&lt;/code&gt; calls with no pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't dispatch a fetch every time a screen mounts.&lt;/strong&gt; If I navigate to a detail screen, go back, and navigate to the same detail screen, I shouldn't see a loading spinner again. A simple &lt;code&gt;if (!data[id])&lt;/code&gt; check before your &lt;code&gt;dispatch(fetchDetails(id))&lt;/code&gt; is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests: quality over coverage
&lt;/h2&gt;

&lt;p&gt;You don't need 90% coverage. You need &lt;em&gt;meaningful&lt;/em&gt; tests. Three good tests beat twenty snapshot tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I want to see:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Business logic&lt;/td&gt;
&lt;td&gt;If there's a rule (max 6 in a list, no duplicates), test it. Reducers and selectors are the highest-value tests.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User interactions&lt;/td&gt;
&lt;td&gt;Render a component with RNTL, press a button, check the result. Use &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;fireEvent&lt;/code&gt;, &lt;code&gt;waitFor&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge cases&lt;/td&gt;
&lt;td&gt;What happens when you add a duplicate? When the list is empty? At the pagination boundary?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passing tests&lt;/td&gt;
&lt;td&gt;Run them before you submit. Failing tests signal unfinished work.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What I don't want to see:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;Snapshot tests everywhere.&lt;/strong&gt; They break on every UI change and prove nothing about behaviour.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Tests that mock everything.&lt;/strong&gt; If your test mocks the function it's testing, it's testing the mock.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;No tests at all.&lt;/strong&gt; This is a hard one to recover from in the walkthrough.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; 5-10 focused tests covering the critical paths. Reducers, selectors, key interactions. That's enough.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Handle loading, errors, and empty states
&lt;/h2&gt;

&lt;p&gt;This is where candidates stand out. Anyone can build the happy path. The question is: what happens when things go wrong?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Loading&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Show a spinner or skeleton on first load. Show a subtle indicator during pagination. Don't flash a full-screen spinner for 100ms.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the API fails, tell the user. A retry button is better than nothing. An informative message is better than "Something went wrong."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Empty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the list is empty or there are no saved items, show something useful. Not a blank screen.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;🚩 &lt;strong&gt;Red flag:&lt;/strong&gt; The app crashes on a slow network. No loading state, no error handling. The reviewer opens DevTools, throttles the network, and the app falls apart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The API call matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GraphQL vs REST:&lt;/strong&gt; if the brief offers both, GraphQL is the stronger choice. It shows you can work with modern API patterns. But a well-implemented REST client beats a messy GraphQL setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use FlatList or FlashList. Never ScrollView for lists.&lt;/strong&gt; &lt;code&gt;ScrollView&lt;/code&gt; renders every item at once. With 100+ items, you'll see frame drops, memory spikes, and eventual crashes. &lt;code&gt;FlatList&lt;/code&gt; virtualises the list, only rendering what's on screen. If I see a &lt;code&gt;ScrollView&lt;/code&gt; wrapping a &lt;code&gt;.map()&lt;/code&gt; for a data list, it suggests a gap in understanding React Native's rendering model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other things that get noticed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Caching: don't refetch data you already have&lt;/li&gt;
&lt;li&gt;✅ Pagination: don't fetch 1000 items on first load&lt;/li&gt;
&lt;li&gt;✅ ErrorBoundary: catches JavaScript errors and shows a fallback instead of a white screen&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Edge cases are where you stand out
&lt;/h2&gt;

&lt;p&gt;The happy path is the minimum. What separates a Software Engineer submission from a Senior one is edge case handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full list?&lt;/strong&gt; What happens when someone tries to add a 7th item? A toast, a disabled button, a modal. Anything except silently failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty list?&lt;/strong&gt; Show a meaningful empty state, not a blank screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rapid taps?&lt;/strong&gt; Does pressing "add" five times fast cause duplicates or crashes?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back navigation?&lt;/strong&gt; When I go from detail back to the list, is my scroll position preserved?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End of list?&lt;/strong&gt; Does pagination stop cleanly when there's no more data?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to handle all of these. But handling &lt;em&gt;some&lt;/em&gt; of them shows you think about real users, not just passing requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  The README is part of the test
&lt;/h2&gt;

&lt;p&gt;Write a README. Not a novel. A short document that covers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;What to write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How to run it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;yarn install&lt;/code&gt;, &lt;code&gt;yarn ios&lt;/code&gt;, done. Extra steps documented.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What you built&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One paragraph summary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Decisions you made&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Why this state management? Why this folder structure? Two sentences each.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What you'd improve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;This is the most important section. It shows self-awareness.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The "what I'd improve" section is a cheat code.&lt;/strong&gt; It lets you acknowledge shortcuts without the reviewer discovering them as flaws. &lt;em&gt;"With more time, I'd add E2E tests with Detox and implement proper caching"&lt;/em&gt; turns a missing feature into a demonstration of judgement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The walkthrough: this is where jobs are won
&lt;/h2&gt;

&lt;p&gt;If the test has a walkthrough call, prepare for it. The code got you into the room. The walkthrough gets you the offer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know your code.&lt;/strong&gt; If I say "show me where you handle the API response," you should navigate there in under 5 seconds. If you hesitate, it can raise questions about how well you know the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explain your trade-offs.&lt;/strong&gt; Don't wait for me to ask. When you show a section of code, say &lt;em&gt;"I chose this approach because X, but I know the trade-off is Y."&lt;/em&gt; That's the answer I'm looking for before I even ask the question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be honest about shortcuts.&lt;/strong&gt; &lt;em&gt;"I used Context here because it was faster, but in a production app I'd move to Zustand once the state got more complex."&lt;/em&gt; That's a strong answer. &lt;em&gt;"I think Context is the best approach"&lt;/em&gt; is a weaker one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have a list of improvements.&lt;/strong&gt; When I ask "what would you change with more time?" the worst answer is "nothing, I'm happy with it." The best answer is a prioritised list: &lt;em&gt;"First I'd add caching, then E2E tests, then refactor to feature-first folders."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask questions back.&lt;/strong&gt; The best walkthroughs are conversations, not presentations. Ask about the team's architecture, their testing approach, their deployment process. It shows you're evaluating the role too, not just hoping to pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stretch goals: do them, but do them well
&lt;/h2&gt;

&lt;p&gt;If the brief mentions optional extras, pick one or two that you can do &lt;em&gt;well&lt;/em&gt;. Don't try to do all of them poorly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Worth picking&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Search/filter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quick to implement, immediately visible, shows UX thinking.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Labels, roles, contrast. Most candidates skip this. Even basic accessibility makes you stand out.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error/offline handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A retry button when the network fails. Shows real-world thinking.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Avoid unless you can do them properly&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Animations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Half-finished animations look worse than no animations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dark mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If it's not consistent across every screen, it's a liability.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;One well-executed stretch goal is worth more than three half-finished ones.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The mistakes that actually cost people the job
&lt;/h2&gt;

&lt;p&gt;These aren't about code quality. They're about signals.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Why it hurts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Not reading the brief properly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing a core requirement. Building two screens when the brief says three.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No tests at all&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Even two or three tests show you care about quality. Zero is a strong negative signal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI-generated code you can't explain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Using AI to help is fine. Submitting code you don't understand is not. This becomes apparent during the walkthrough.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overengineering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A tech test doesn't need a design system and a micro-frontend architecture. Build what the brief asks for, well.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Submitting late without communicating&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If you need more time, ask. Going silent and submitting three days late is a red flag.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The one thing that matters most
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Show that you think.&lt;/strong&gt; Not just that you code.&lt;/p&gt;

&lt;p&gt;Anyone can build screens. The candidates who get hired are the ones who demonstrate judgement: why they chose this approach, what they'd do differently, where the code would break at scale, what tests actually matter.&lt;/p&gt;

&lt;p&gt;The tech test isn't testing whether you can write React Native. It's testing whether you can make good decisions and communicate them clearly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build something clean, test the important parts, document your thinking, and be ready to talk about it honestly. That's it. That's the whole secret.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;We're hiring!&lt;/strong&gt; We're looking for React Native engineers to join the Mobile Platform team at Hargreaves Lansdown. &lt;a href="https://warrendeleon.com/hiring/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=pass-rn-tech-test" rel="noopener noreferrer"&gt;View open roles&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>hiring</category>
      <category>careeradvice</category>
    </item>
    <item>
      <title>Why I redesigned our React Native tech test in my first week</title>
      <dc:creator>Warren de Leon</dc:creator>
      <pubDate>Sun, 29 Mar 2026 14:48:16 +0000</pubDate>
      <link>https://dev.to/warrendeleon/why-i-redesigned-our-react-native-tech-test-in-my-first-week-hhk</link>
      <guid>https://dev.to/warrendeleon/why-i-redesigned-our-react-native-tech-test-in-my-first-week-hhk</guid>
      <description>&lt;h2&gt;
  
  
  A test built for a different time
&lt;/h2&gt;

&lt;p&gt;Four days before I officially started at Hargreaves Lansdown, I went into the office for a passport check. While I was there, my manager mentioned I'd be hiring a team. My first question was whether I could change the interview process. He said yes. &lt;em&gt;I hadn't even had my first day yet.&lt;/em&gt; By the time I started on the 23rd, I was already building the new test.&lt;/p&gt;

&lt;p&gt;I'm the new Engineering Manager for the &lt;strong&gt;Mobile Platform&lt;/strong&gt; squad. We're rebuilding HL's mobile app in React Native, a brownfield migration from the existing native iOS and Android apps. I need engineers who can work at the platform level.&lt;/p&gt;

&lt;p&gt;I didn't need to ask to see the tech test. I'd been through it myself just weeks earlier. It's how HL hired &lt;em&gt;me&lt;/em&gt;: a live coding exercise where you build a small app in about an hour with the interviewer watching, followed by technical questions from a questionnaire. The whole interview ran about 90 minutes.&lt;/p&gt;

&lt;p&gt;The test made sense for its original context. When the team was smaller and hiring for different roles, it was a reasonable way to screen candidates quickly. But our needs had changed. We weren't hiring someone to build simple screens anymore. We were hiring &lt;strong&gt;platform engineers&lt;/strong&gt; who'd own the architecture that every other mobile team at HL would ship through.&lt;/p&gt;

&lt;p&gt;I needed the test to answer different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can they structure a &lt;strong&gt;multi-screen app&lt;/strong&gt; with navigation that doesn't fall apart?&lt;/li&gt;
&lt;li&gt;Can they call a &lt;strong&gt;real API&lt;/strong&gt; and handle what happens when the network fails?&lt;/li&gt;
&lt;li&gt;Do they write &lt;strong&gt;tests&lt;/strong&gt; because they care about working software, or because someone told them to?&lt;/li&gt;
&lt;li&gt;Can they sit across from me and explain &lt;em&gt;why&lt;/em&gt; they built it that way?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The existing test was designed for different questions. I needed to build something around ours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits of live coding
&lt;/h2&gt;

&lt;p&gt;Live coding can tell you whether someone codes comfortably under observation. For some roles, that matters. For ours, I needed to see something different.&lt;/p&gt;

&lt;p&gt;I've been on both sides. As recently as January this year, I bombed a live coding exercise for a role I was perfectly qualified for. The problem was simple. I knew how to solve it. But with someone watching my every keystroke, my mind went blank. &lt;em&gt;I didn't pass.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As an interviewer, I've watched the same thing happen to candidates. Strong engineers who freeze on problems they'd solve in five minutes at their own desk. Live coding measures composure under observation. That's a valid signal for some roles, but it wasn't the signal I needed.&lt;/p&gt;

&lt;p&gt;For a platform engineering role, where the work is architecture decisions, design system components, and CI/CD pipelines, I wanted to see how candidates approach problems with time and context. &lt;strong&gt;The kind of thinking the job actually requires.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Showing vs telling
&lt;/h2&gt;

&lt;p&gt;The previous process also included a technical questionnaire. The interviewer would pick questions from a reference sheet covering React Native architecture, state management, testing strategies, and platform differences, then compare answers against expected responses. Sometimes candidates would naturally cover the topics during the live coding, and the interviewer would skip those questions.&lt;/p&gt;

&lt;p&gt;These are all valid topics. They're &lt;em&gt;exactly&lt;/em&gt; the things I want my engineers to understand. Asking someone to explain a concept tells you whether they understand the theory. Seeing how they apply it in their own code gives you a different kind of signal.&lt;/p&gt;

&lt;p&gt;The new process tests the same topics through the candidate's own code. Instead of asking &lt;em&gt;"how would you structure navigation in a complex app?"&lt;/em&gt;, I can open their submission and see how they approached it, then have a richer conversation about the choices they made. The walkthrough still covers architecture, trade-offs, and technical depth, but it's grounded in something the candidate &lt;em&gt;built&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built instead
&lt;/h2&gt;

&lt;p&gt;I designed a take-home assessment. A small but real app: multiple screens, a public API, navigation, state management with actual business rules, TypeScript throughout. Not a toy. Not a weekend project either. Something that requires &lt;strong&gt;genuine architectural thinking&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Four principles guided the design:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mirror the actual job.&lt;/strong&gt; The test should feel like the work. If a candidate can build this app, they can contribute to our codebase on day one. If they can't, that's useful information too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remove the boilerplate tax.&lt;/strong&gt; I give candidates a fully configured starter project. TypeScript, ESLint, Prettier, Jest, React Native Testing Library, path aliases. &lt;em&gt;All set up.&lt;/em&gt; I don't care whether someone can configure a bundler. I care whether they can write application code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be clear about what, not how.&lt;/strong&gt; The brief explains what the app should do. It never says which state management library to use, how to structure the folders, or which API client to pick. Those decisions are the most revealing part of the submission. A candidate who picks Redux Toolkit for a three-screen app tells me something different from one who picks Zustand or React Context. Neither is wrong. &lt;em&gt;Both are interesting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respect people's time.&lt;/strong&gt; Candidates get a week. The work should take 4 to 6 hours. People have jobs, families, lives. No one should have to take a day off to do a tech test for a company that might not hire them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The walkthrough is where the magic happens
&lt;/h2&gt;

&lt;p&gt;The take-home code is half the evaluation. The other half is a walkthrough call: the candidate &lt;strong&gt;demos the app&lt;/strong&gt;, runs their tests live, and walks through the code.&lt;/p&gt;

&lt;p&gt;This is where you learn how deeply someone understands what they built. In the age of AI-assisted development, that understanding matters more than ever.&lt;/p&gt;

&lt;p&gt;Three things I'm looking for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ownership.&lt;/strong&gt; &lt;em&gt;"Navigate to the file where you handle the API response."&lt;/em&gt; If they wrote it, they'll jump straight there. If they're not fully comfortable with the codebase, that becomes clear quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off thinking.&lt;/strong&gt; I ask about every significant decision. &lt;em&gt;"Why this state management approach?"&lt;/em&gt; The answer I want isn't "because it's the best." The answer I want is &lt;em&gt;"because it fits this scope, but here's where it would break down, and here's what I'd move to."&lt;/em&gt; Engineers who think in trade-offs build better systems than engineers who think in absolutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-awareness.&lt;/strong&gt; &lt;em&gt;"What would you change if you had more time?"&lt;/em&gt; Strong candidates light up at this question. They have a list. They know where they cut corners. They know what's fragile. They've been thinking about improvements since they submitted. Less experienced candidates tend to say &lt;em&gt;"I'm happy with it"&lt;/em&gt; and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured scoring
&lt;/h2&gt;

&lt;p&gt;One thing I wanted from day one was a &lt;strong&gt;structured scorecard&lt;/strong&gt;. When you're scaling a team and multiple people are involved in hiring, everyone needs to evaluate the same things in the same way. Without that, two interviewers can review the same candidate and reach different conclusions because they're weighting different things.&lt;/p&gt;

&lt;p&gt;I built a scorecard that breaks the evaluation into weighted sections: does the app work, is the data layer sound, is the code well-structured, are there tests, and can the candidate explain it all in the walkthrough. Each section has specific criteria on a consistent scale. &lt;strong&gt;Every interviewer evaluates the same things in the same order.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The scorecard also maps scores to levels. A number tells you whether someone is Graduate, Associate, Software Engineer, or Senior level. This removes ambiguity from the levelling conversation. The rubric does the thinking. The humans verify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Senior candidates get a harder round
&lt;/h2&gt;

&lt;p&gt;For senior hires, there's an additional &lt;strong&gt;system design&lt;/strong&gt; conversation. No whiteboard. No &lt;em&gt;"design Twitter in 45 minutes."&lt;/em&gt; We talk through real scenarios relevant to the platform we're building. What changes when 20 teams build on the same mobile platform? How do you handle shared dependencies? What's your approach to backwards compatibility?&lt;/p&gt;

&lt;p&gt;It's a conversation between two engineers, not a performance for an audience. The best candidates &lt;strong&gt;push back&lt;/strong&gt; on my assumptions and ask clarifying questions. That's exactly the behaviour I want from a senior on the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Early days
&lt;/h2&gt;

&lt;p&gt;In my first week at HL, I hired a Senior Engineer through the existing process (that happened on day two, before the new test was ready). Going forward, the new process is the standard for all React Native hiring across the UCX-Core tribe. My peer EM, who runs another squad, reviewed the test and the scorecard and agreed to adopt it for his team's hires too. That's the advantage of a well-documented system: &lt;strong&gt;it scales beyond one manager's squad.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm about to hire two Software Engineers using the new process. Every candidate will get the same test, the same starter project, the same evaluation criteria, and the same scoring rubric. The bias surface area shrinks when you standardise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;If you're joining a new team as an engineering manager, &lt;strong&gt;look at the hiring process early&lt;/strong&gt;. Don't wait until you've "learned the codebase" or "understood the culture." Hiring is one of the highest-leverage activities you have. Every person you bring on shapes the team for years.&lt;/p&gt;

&lt;p&gt;And if your tech test no longer matches what you're hiring for, it's worth revisiting. The best hiring processes evolve alongside the team's needs.&lt;/p&gt;

&lt;p&gt;Design a test that mirrors the actual job. Give candidates a starter project so you're testing &lt;em&gt;engineering&lt;/em&gt;, not &lt;em&gt;configuration&lt;/em&gt;. Make the requirements clear but let them make their own decisions. Then sit across from them and ask &lt;strong&gt;&lt;em&gt;why&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The combination of thoughtful take-home code and a structured walkthrough gives you more signal in two hours than any live coding exercise gives you in two days.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;If you're preparing for a React Native tech test, I wrote a companion post with practical advice: &lt;a href="https://warrendeleon.com/blog/how-to-pass-a-react-native-tech-test/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=rn-tech-test-redesign" rel="noopener noreferrer"&gt;How to pass a React Native tech test&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;We're hiring!&lt;/strong&gt; We're looking for React Native engineers to join the Mobile Platform team at Hargreaves Lansdown. &lt;a href="https://warrendeleon.com/hiring/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=rn-tech-test-redesign" rel="noopener noreferrer"&gt;View open roles&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>engineeringmanagement</category>
      <category>hiring</category>
      <category>reactnative</category>
    </item>
  </channel>
</rss>
