Guides / Detox

How to test SMS OTP in React Native with Detox

Updated October 8, 2026
Short answer

A React Native app does not send SMS itself; it calls your backend, and the backend calls an SMS provider. Point that provider at a mock inbox in the test backend. In the Detox test, which runs in Jest on Node, use a unique fake number, tap "send code", read the code with the OtpMock helper and type it with typeText.

Step 1: route the backend's SMS to the mock

The flow is: app calls POST /auth/send-code on your backend, the backend asks Twilio, Vonage or another provider to send the SMS. In the test or staging backend, point that provider's SDK at otpmock with one option; each provider's option is on its provider page. Make sure the Detox build of the app (for example a debug or e2e configuration) talks to that backend. Nothing in the app itself changes.

Step 2: add the helper to your e2e folder

Download the dependency-free helper otpmock.ts (or otpmock.mjs) into e2e/. It uses the global fetch available in Node 18+, which is where Jest runs your Detox tests. It calls:

HTTP
GET https://api.otpmock.com/v1/inbox/{phone}/code?since={unix_ms}
Authorization: Bearer $OTPMOCK_API_KEY

200 {"code":"482913","messageSid":"SM…","body":"Your code is 482913","receivedAt":1791417652342}

It polls every 300 ms while the API answers 404 and throws after the timeout (15 s by default).

Step 3: the Detox test

e2e/signup.test.ts
import { by, device, element, expect } from "detox";
import { OtpMock } from "./otpmock";

const otp = new OtpMock({
  baseUrl: process.env.OTPMOCK_URL ?? "https://api.otpmock.com",
  apiKey: process.env.OTPMOCK_API_KEY!,
});

describe("phone verification", () => {
  beforeEach(async () => {
    await device.launchApp({ newInstance: true, delete: true });
  });

  it("verifies the phone number with an SMS code", async () => {
    const phone = otp.randomPhone(); // +1555XXXXXXX, unique per test

    await element(by.id("phone-input")).typeText(phone);
    const since = Date.now() - 5_000; // margin for clock skew
    await element(by.id("send-code-button")).tap();

    const { code } = await otp.waitForCode(phone, { since, timeout: 20_000 });

    await element(by.id("code-input")).typeText(code);
    await element(by.id("verify-button")).tap();
    await expect(element(by.id("welcome-screen"))).toBeVisible();
  });
});

by.id matches the component's testID prop. The waitForCode call runs in Node on the test runner, not inside the app, so the simulator or emulator never talks to otpmock.

Passing the API key

Jest inherits the environment of the detox test process, so export the key before running:

shell
export OTPMOCK_API_KEY=...   # from your CI secret store
detox test --configuration ios.sim.debug

If your Jest config transforms TypeScript only through Babel, the helper works as is. If you use plain JavaScript tests, import otpmock.mjs or convert the helper to CommonJS to match your Jest setup.

SMS autofill and one-time-code fields

With textContentType="oneTimeCode" (iOS) or autoComplete="sms-otp" (Android), real devices offer the received code above the keyboard. With a mock inbox no SMS reaches the device, so that suggestion never appears and the test types the code. If your screen relies on a library that auto-reads SMS (SMS Retriever), make sure the manual input path still works, since that is what the test exercises.

Parallel workers

Detox can run several simulators with Jest workers (--maxWorkers). Each test generates its own number, so workers never read each other's codes and no inbox cleanup is needed between tests.

Common pitfalls

FAQ

Does this work with Expo apps?

Yes. Detox tests Expo prebuild or bare builds the same way; only the backend's SMS provider setting matters.

Do I need to change React Native code?

No. Add testID props for the selectors if they are missing; the SMS change is in the backend.

What if my backend uses Firebase phone auth?

Firebase sends the SMS itself, so it cannot be pointed at otpmock. Use Firebase's test phone numbers for that path.

Try it on your own suite

The free plan includes 100 messages a month. No card required.

Get a free API key

Related guides