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
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Rate limit | Resend spam | 429 in prod | Probe cooldown_seconds |
| Wrong code | Lockout after N | Account stuck | Seed attempt count; probe locked |
| Expired OTP | TTL untested | Valid code rejected wrongly | Arrange backdated code |
| Parallel CI | Same phone number | Cross-test OTP | Unique test number per worker |
| International | E.164 format | Send fails | Twilio test numbers |
| MFA step-up | SMS after password | Session partial | Probe mfa_verified flag |
| Voice fallback | SMS disabled | Untested path | Separate scenario |
| Provider stub | No real Twilio in CI | Integration gap | Test 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
+15005550006simulate 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
| Case | Assert |
|---|---|
| Wrong code 3× | Probe locked or cooldown |
| Expired code | Probe phoneVerified false |
| Resend before cooldown | UI error; probe no new SMS count increment |
| Valid code after verify | Second 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-pattern | Why it fails | Better approach |
|---|---|---|
| Real user phones in CI | PII + cost | Twilio test numbers |
| Shared +1 number | OTP cross-talk | Per-worker phone suffix |
| Skip lockout test | Brute force risk | Probe after N failures |
| Assert SMS sent via UI only | Provider failed silently | Probe otp_sent log |
| Hardcode prod Twilio creds | Billing incident | Test 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.
- Arrange: Twilio test creds; seed user with phone; known test code.
- Act: Login password then submit OTP in UI.
- 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).
Related scenarios
- MFA / 2FA — TOTP and backup codes
- Transactional email — parallel channel
- Firebase auth — phone auth emulator
- Captcha flows — bot protection on resend
External references
- Twilio test credentials
- Twilio Verify test mode — check test behavior for your integration
- Firebase phone auth testing — if using Firebase instead
- Playwright test parallel — isolate phone per worker
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.