A number maker and a message gateway are different things

A phone number maker returns a value for a test, demo, or fixture. It does not reserve a telephone line, create an inbox, or tell a carrier to reject a message. If an application takes that value and calls a live SMS API, the gateway sees a destination string; it cannot infer that the string came from a test generator.

Keep these two behaviors separate in your design: generating input is safe local data work, while sending a message is an external side effect. Do not use “looks fake” as the gate. Number plans change, and an arbitrary plausible string could be allocated now or later.

This distinction also matters for searches such as phone number maker for verification. A generated example cannot receive an OTP. For an account-verification flow, use the authentication provider’s documented test mode or a phone line your team controls and is authorized to use. Avoid sending test codes to public or shared inboxes.

Route each test through the right boundary

Choose the message behavior from the environment, not from the generated value. The generator can exercise formatting and application branches; only a controlled delivery setup should cross the provider boundary.

Test layerPhone inputMessage pathWhat the result proves
Local form or parser testSynthetic fixture from the phone number makerBlocked transport; no provider credentials loadedInput handling, formatting, and validation behavior
Continuous integrationFixed fixtures plus invalid and boundary casesBlocked transport; network send is an assertion failureRegression behavior without sending a message
Provider API contract testProvider-specific test inputsProvider simulator or test credentials, when supportedDocumented API responses, not carrier delivery
Authorized delivery checkA team-controlled test lineSeparate sandbox account and explicit recipient allowlistOnly the configured end-to-end delivery path

Do not route routine fixtures into a real messaging account as a shortcut. A format check, an API response test, and a delivered SMS are different claims and need different evidence.

Make the default transport fail closed

Put the guard where all message requests pass. In ordinary tests, inject a transport that has no provider client. Select a sandbox transport only through an explicit test mode; missing sandbox configuration must never fall back to production credentials.

export interface SmsTransport {
  send(to: string, body: string): Promise<void>;
}

export class BlockedSmsTransport implements SmsTransport {
  async send(): Promise<void> {
    throw new Error("Outbound SMS is disabled in this test environment.");
  }
}

function makeSmsTransport(config: TestConfig): SmsTransport {
  if (config.mode !== "provider-sandbox") {
    return new BlockedSmsTransport();
  }
  if (!config.sandbox || config.allowlist.size === 0) {
    throw new Error("Sandbox mode needs a client and an explicit allowlist.");
  }
  return new AllowlistedSandboxTransport(config.sandbox, config.allowlist);
}

// Local and CI select the blocked transport; sandbox is an explicit opt-in.
const smsTransport = makeSmsTransport(testConfig);

The branch above is intentionally one-way: an absent flag, missing sandbox credentials, or an unknown environment selects the blocked path. Keep test credentials and live credentials separate, and never let a fallback silently select the live client.

Add a guard test at the same boundary. It should fail if a routine test can call the provider client, even when the rest of the verification flow returns a success-shaped response.

it("does not call the provider in routine tests", async () => {
  const sandboxClient = { send: vi.fn() };
  const transport = makeSmsTransport({
    mode: "ci",
    sandbox: sandboxClient,
    allowlist: new Set()
  });

  await expect(
    transport.send(testFixture.e164, "test message")
  ).rejects.toThrow("Outbound SMS is disabled");

  expect(sandboxClient.send).not.toHaveBeenCalled();
});

Use a fixture reference in the test rather than a number that merely looks fictional. The test should prove that the provider boundary was not crossed; it should not depend on a national numbering prefix being permanently unassigned.

Use a provider sandbox only for the claim it supports

Provider test modes are not interchangeable. Twilio documents test credentials for a supported set of API resources; those requests can exercise SMS API behavior without sending an SMS or connecting to a real number. The same page warns that newly created credentials are not available in the new console, so check the current account and product documentation before designing around that legacy mode. Twilio also documents a Virtual Phone that simulates a device without using a carrier network or a real recipient device.

These tools can help test a provider request or message rendering. They do not prove that a carrier delivered a message to a real handset. If delivery itself is the requirement, use a sandbox workflow or a controlled line your team owns, and allow only that destination for the run.

A developer question on Stack Overflow illustrates the uncertainty that can arise when a real-looking phone number is added to an authentication test allowlist: the author asked whether a message could still reach that device and how a fictional example should be formed. That old question is evidence of a testing concern, not current provider guidance; follow the provider’s current documentation for actual behavior.

Release checks for a safe test run

  • Local and CI jobs select the blocked transport unless a provider sandbox is named explicitly.
  • Sandbox credentials are distinct from production credentials; a missing sandbox setting fails closed.
  • Sandbox recipients are limited to team-controlled test lines, not arbitrary values from a generator.
  • Logs record the environment, provider request ID, and test case ID, while masking full phone values and message bodies.
  • Production code never accepts synthetic fixture files as a delivery-recipient source.

If a test unexpectedly creates a provider request, stop the run and inspect its environment and transport selection before running it again. Keep the incident record to what is needed to trace the configuration; avoid copying full phone values or OTPs into shared logs.

Common questions

Can a phone number maker receive a verification code?

No. It can make a phone-shaped test value, but it does not assign a line or host an inbox. Use a provider’s documented authentication test mode or an authorized number you control.

Could a generated number belong to someone?

A plausible format does not prove non-assignment. Treat arbitrary generated values as potentially real destinations and block outbound traffic in routine tests.

How can I test an SMS without sending it?

Use a mocked transport for application tests and a provider-supported simulator or test credential for the API behavior it documents. For actual delivery, use a controlled sandbox recipient and a separate, allowlisted run.

References and related guides