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 caseMock verifier outcomeExpected app result
Correct code, first submissionaccept_onceMark this challenge verified; consume its secret.
Same code submitted againalready_usedReject the replay; keep the account unverified.
Challenge past its expiryexpiredReject it and offer the documented retry path.
Incorrect attempts reach the limitthrottledStop further guesses and preserve the account-level attempt count.
Resend creates a new challengenew_challengeApply 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