QA Automation • 4 min read • By Marcus Vance

How to Test SMS Verification Flows Without Real Numbers

Architectural patterns for mocking one-time passcodes (OTP), simulating network latency, and safely running end-to-end verification tests in CI/CD.

Table of Contents

Quick jump to any section in this article

8 sections

How to Test SMS Verification Flows Without Real Numbers

Testing authentication systems that rely on SMS One-Time Passcodes (OTP) is one of the most frustrating challenges in modern web and mobile automation.

Connecting automated test runners (like Cypress, Playwright, or Appium) to physical SIM cards leads to carrier rate limiting, expensive per-message telecom billing, network transit flakiness, and occasional carrier spam filtering.

Here is how modern engineering organizations reliably test two-factor authentication and SMS verification flows without burning real numbers or paying carrier fees.


The Cost and Flakiness of Real SIM Testing

Automating real SMS delivery in automated pipelines introduces severe vulnerabilities:

  • Financial Cost: Sending 50,000 SMS verification messages across a multi-stage CI/CD pipeline costs thousands of dollars monthly.
  • Telecom Latency: Cellular networks routinely experience 5 to 30 second delivery latencies, causing end-to-end tests to fail due to timeout thresholds.
  • Carrier Throttling: Cellular carriers flag repetitive automated verification codes as commercial spam, blocking staging numbers without warning.

1. Designated Synthetic Number Ranges

The foundation of safe SMS testing begins with non-routable synthetic phone numbers. Telecom regulators reserve specific number ranges that are guaranteed never to connect to a real subscriber line.

Standard Test Number Allocations

  • North America (NANP): +1 (NPA) 555-0100 through +1 (NPA) 555-0199 (e.g., +1-415-555-0142).
  • United Kingdom (Ofcom): 07700 900000 through 07700 900999 (allocated specifically for drama and media).
  • Australia (ACMA): 0491 570 156 through 0491 570 159 (reserved for fictional representations).

By routing test flows exclusively to these ranges, your application can execute complete form input validation, formatting checks, and database indexing without any risk of bothering actual phone users.


2. In-Memory Mocking Gateways

Rather than invoking external SMS APIs during automated tests, configure your backend notification service with an in-memory mock adapter.

Mocking Twilio / AWS SNS Webhooks

In your staging environment, inject a mock gateway service:

// Notification service adapter interface
interface SmsGateway {
  sendOtp(to: string, code: string): Promise<boolean>;
}

// In-Memory Test Implementation
export class InMemoryTestSmsGateway implements SmsGateway {
  private static otpStore = new Map<string, string>();

  async sendOtp(to: string, code: string): Promise<boolean> {
    // Store in-memory for assertion by test runners
    InMemoryTestSmsGateway.otpStore.set(to, code);
    console.log(`[TEST GATEWAY] Captured OTP for ${to}: ${code}`);
    return true;
  }

  static getLatestCode(to: string): string | undefined {
    return this.otpStore.get(to);
  }
}

Your automated test runner can now query a test-only internal endpoint (GET /api/test/otp?phone=+14155550142) to retrieve the exact generated passcode in under 5 milliseconds.


3. Dedicated Sandbox Testing Gateways

Major cloud telecommunication providers offer dedicated sandbox modes:

  • Twilio Magic Numbers: Twilio provides specialized test numbers (such as +15005550006 for valid delivery, or +15005550001 to simulate invalid numbers) that return instant mock API responses without billing your account.
  • AWS SNS Sandbox: Restricts outbound messages to verified test numbers or allows mocking SNS topics via LocalStack for complete local emulation.

End-to-End Test Examples with Playwright and Cypress

Here is an end-to-end Playwright test demonstrating a zero-cost, instant SMS verification flow:

import { test, expect } from '@playwright/test';

test('completes phone verification with synthetic test number', async ({ page, request }) => {
  const testPhone = '+14155550142';

  // 1. Visit signup and enter synthetic test number
  await page.goto('/signup');
  await page.fill('input[name="phone"]', testPhone);
  await page.click('button[type="submit"]');

  // 2. Fetch the generated OTP from test endpoint
  const otpResponse = await request.get(`/api/testing/get-otp?phone=${encodeURIComponent(testPhone)}`);
  const { code } = await otpResponse.json();

  // 3. Enter code and assert verified state
  await page.fill('input[name="otp"]', code);
  await page.click('button#verify-btn');

  await expect(page.locator('.verification-success')).toBeVisible();
});

Security Precautions in Pre-Production Environments

When implementing OTP mock backdoors:

  • Strict Environment Isolation: Ensure mock OTP endpoints are physically absent from production bundles using build flags (process.env.NODE_ENV !== 'production').
  • Firewall Restrictions: Expose test endpoints exclusively on private VPCs or localhost test runners.
  • Pattern Matching: Enforce that mock endpoints only return codes for officially designated synthetic test ranges (555-01xx).