Skip to main content

How to Test SMS and OTP Verification

Short answer

Real SMS in CI is costly, rate-limited, and unsafe for parallel workers—yet untested OTP flows leak security bugs (reuse, no lockout, wrong tenant). Use Twilio test credentials and magic test phone numbers with fixed codes, or virtual-number APIs that expose last message to your test account—never scrape production user phones.

Part of Testing Guides by integrations.

Who this is for

Apps with phone verification, SMS MFA, or passwordless OTP via Twilio, Vonage, AWS SNS, or similar—not email-only auth (see transactional email).

Why testing SMS OTP matters

  • Security — OTP reusable; no attempt lockout; predictable codes in prod
  • Support load — resend cooldown missing → SMS bill spike + user frustration
  • Compliance — opt-in not recorded; MFA bypass via stale session
  • CI cost — real SMS sends burn budget and hit rate limits

Complexity map

ScenarioEdge caseWhy tests breakApproach
Rate limitResend spam429 in prodProbe cooldown_seconds
Wrong codeLockout after NAccount stuckSeed attempt count; probe locked
Expired OTPTTL untestedValid code rejected wronglyArrange backdated code
Parallel CISame phone numberCross-test OTPUnique test number per worker
InternationalE.164 formatSend failsTwilio test numbers
MFA step-upSMS after passwordSession partialProbe mfa_verified flag
Voice fallbackSMS disabledUntested pathSeparate scenario
Provider stubNo real Twilio in CIIntegration gapTest credentials + stub mode

Twilio test credentials

From Twilio test credentials docs:

  • Use Test Account SID and Test Auth Token—no real SMS charges
  • Magic numbers like +15005550006 simulate success; invalid numbers simulate failure
  • Verification APIs may return fixed codes in test mode—verify against current Twilio docs for your SDK version
// Arrange: point app TWILIO_* env to test credentials in CI
process.env.TWILIO_ACCOUNT_SID = process.env.TWILIO_TEST_ACCOUNT_SID;
process.env.TWILIO_AUTH_TOKEN = process.env.TWILIO_TEST_AUTH_TOKEN;

Never commit live Auth Token to repo.

Pattern: fixed code with test number

Many teams configure test phone numbers in provider console with fixed OTP (similar to Firebase test phones):

const phone = `+1555000${String(test.info().parallelIndex).padStart(4, '0')}`;
await seedUser({ phone, phoneVerified: false });

await page.getByLabel('Phone').fill(phone);
await page.getByRole('button', { name: /send code/i }).click();

// Twilio test mode / configured test number returns known code
await page.getByLabel(/code/i).fill('123456');
await page.getByRole('button', { name: /verify/i }).click();

await expect.poll(() => probeUser(phone)).toMatchObject({ phoneVerified: true });

Document expected test code in repo README for QA—rotate if provider changes behavior.

Reading OTP from virtual inbox (integration tests)

If using real test creds with programmable inbox (Twilio logs, Mailosaur SMS):

async function fetchLatestSms(to: string) {
return await expect.poll(async () => {
const messages = await twilio.testClient.messages.list({ to, limit: 1 });
return messages[0]?.body?.match(/\b(\d{6})\b/)?.[1];
}, { timeout: 20_000 }).toBeTruthy();
}

Prefer fixed test numbers over parsing when available—less flake.

Negative scenarios

CaseAssert
Wrong code 3×Probe locked or cooldown
Expired codeProbe phoneVerified false
Resend before cooldownUI error; probe no new SMS count increment
Valid code after verifySecond use fails

Stub provider in test env

For speed, route SMS send to test double that writes OTP to Redis keyed by runId:

// POST /api/test/last-otp?phone=...
const { code } = await request.get(`/api/test/last-otp?phone=${encodeURIComponent(phone)}`).then(r => r.json());

Still run one spec against Twilio test credentials in nightly job for integration parity.

Anti-patterns

Anti-patternWhy it failsBetter approach
Real user phones in CIPII + costTwilio test numbers
Shared +1 numberOTP cross-talkPer-worker phone suffix
Skip lockout testBrute force riskProbe after N failures
Assert SMS sent via UI onlyProvider failed silentlyProbe otp_sent log
Hardcode prod Twilio credsBilling incidentTest Account SID in CI

Example scenario

Situation: User enrolls SMS MFA and must enter OTP to complete login.

Expected outcome: Valid OTP sets mfa_verified; wrong codes lock after policy limit.

Why UI-only automation breaks: UI shows verified but session probe lacks mfa flag—bypass on next API call.

  1. Arrange: Twilio test creds; seed user with phone; known test code.
  2. Act: Login password then submit OTP in UI.
  3. Assert: Probe session mfa_verified; wrong-code spec probes lockout.

TestChimp workflow: Track otp_channel in TrueCoverage when prod prefers SMS over email for MFA.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

How do I read SMS OTP in automated tests?

Use Twilio test magic numbers with fixed codes, console-configured test phones, or test credential APIs that expose last message—never scrape real user devices.

Can I avoid sending any SMS in CI?

Use provider stub that writes OTP to test endpoint keyed by runId for PR speed; keep nightly job with Twilio test credentials for integration confidence.

How do I test parallel OTP flows?

Unique test phone per worker (suffix parallelIndex). Never share one number across tests.

How do I test resend cooldown?

Trigger resend twice rapidly; probe cooldown_seconds or provider send count. Assert UI error without second OTP accepted.

Wrong OTP lockout—how to test?

Submit invalid code N times via UI; probe account locked or increasing backoff. Seed known attempt count via Arrange for edge cases.

Twilio test credentials vs live credentials?

CI must use Test Account SID/Auth Token from Twilio docs—no charges, predictable failures. Live creds only in controlled staging with budget alerts.

SMS preferred over email in prod for MFA—how do we know tests match?

Compare otp_channel distribution in TrueCoverage prod vs test-run. Add SMS scenarios when prod slice lacks coverage.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo