Keep the phone fixture separate from the OTP
A generated phone string is only test input. It does not establish ownership, reachability, or control of a real account. The one-time code is a separate, short-lived secret created by the verification flow. Keep the phone fixture fixed and vary the verifier outcome so each test has one clear purpose.
For a local example, store a reserved fictional number in a named test fixture and label it as non-deliverable. The phone value should never decide whether the application switches from a fake verifier to a live SMS provider.
Write the state contract before the tests
These cases describe application behavior, not a promise that a phone-number generator can receive codes. Pin your own expiry window and resend policy in code. For out-of-band authentication, NIST SP 800-63B says a secret becomes invalid after its validity period, is accepted only once while valid, and that generating a new secret must not reset the failed-attempt count.
| Test case | Mock verifier outcome | Expected app result |
|---|---|---|
| Correct code, first submission | accept_once | Mark this challenge verified; consume its secret. |
| Same code submitted again | already_used | Reject the replay; keep the account unverified. |
| Challenge past its expiry | expired | Reject it and offer the documented retry path. |
| Incorrect attempts reach the limit | throttled | Stop further guesses and preserve the account-level attempt count. |
| Resend creates a new challenge | new_challenge | Apply your stated invalidation policy; do not silently clear failed attempts. |
Do not copy a universal timeout or attempt limit from this table. Choose limits for your system and applicable standard, then make those values explicit in tests.
Use a test-only adapter, not a test phone inbox
Put the provider behind an interface so the test can supply a controlled outcome without contacting a carrier. Inject a clock as well: expiration tests should advance a fake clock instead of sleeping until a real challenge times out.
type OtpScenario =
| "accept_once" | "already_used" | "expired"
| "incorrect" | "throttled" | "new_challenge";
type VerificationCase = {
id: string;
phoneFixtureId: string;
scenario: OtpScenario;
testOnly: true;
};
const testCase: VerificationCase = {
id: "signup-expired-01",
phoneFixtureId: "us-fictional-demo-01",
scenario: "expired",
testOnly: true
};
// The test adapter returns this scenario locally.
// It does not send an SMS or expose a reusable OTP.
const result = await verifierForTests.verify(testCase);
expect(result.status).toBe("expired");
Guard the adapter with the environment as well as the testOnly marker. Production configuration should fail closed if it receives a test scenario, and CI should assert that the mock transport—not the live provider—was selected.
Check the race and clock edges
- Submit the same challenge twice at the same time; only one request may consume it.
- Advance the injected clock just before and just after expiry and verify the boundary consistently.
- Request a resend after failed attempts; verify the attempt policy remains in force.
- Simulate provider timeout and retry; make the app's challenge policy explicit instead of creating duplicate live sends.
- Assert logs contain the case ID and outcome, never a complete phone value or one-time secret.
This matrix is an original test contract for OTP state transitions. It complements the country-aware fictional-range guide, which explains how safe phone fixtures are selected, and the delivery-isolation guide, which explains how to block real SMS in routine testing.
Questions developers ask
Can a fake number receive a real OTP?
No. A fictional test fixture is not a live line. Use an offline verifier for app states or a provider's approved sandbox and authorized test destination when you need to test real delivery.
How do I test an expired code without waiting?
Inject a test clock and move it across the expiry boundary. Keep the application code path under test, but keep time and delivery dependencies controlled.
Should resending a code clear the failed-attempt counter?
Do not assume it should. NIST SP 800-63B says generating a new out-of-band secret must not reset the failed authentication count. Apply the requirements that fit your system and test that rule explicitly.
Sources
- NIST SP 800-63B, Section 3.1.3.2: Out-of-Band Verifiers — validity, single-use secrets, and rate limiting.
- NIST SP 800-63B, Section 3.1.3 — the out-of-band flow and secret lifecycle.
- OWASP Multifactor Authentication Cheat Sheet — SMS and phone-call codes are single-use factors with risks and limitations.
- Developer question about testing phone-code login flows — used only as a question signal; the test contract above is based on the cited standards, not forum replies.
