MailSlurp logo

blog

SMS Testing: Check OTPs, Resends and Login Before Release

Test SMS receipt and the login it enables. Cover fresh codes, leading zeros, resends, expiry and parallel accounts with a runnable OTP example.

A customer requests a login code, receives a text and enters it. An SMS test should follow that whole journey, including the moment the application lets the customer in.

MailSlurp gives your tests private phone numbers that can receive the application's real messages. You can read those messages through the dashboard or API, extract the one-time password (OTP), and submit it through the same form a customer uses. The final check is the authenticated account, not just the presence of six digits in a text.

What should an SMS test cover?

Start with one successful login, then exercise the cases that change which code should work.

  • New login: the current message reaches the assigned number and its code opens the expected account.
  • Leading zero: the code remains a string, preserving every digit when submitted.
  • Resend: the application follows its documented policy for the previous and replacement codes.
  • Expiry: verification is rejected, and a fresh request can still complete the journey.
  • One-time use: the consumed code cannot authorize a second verification.
  • Parallel tests: each account receives and uses its own message.
  • Failed or missing send: the failure is visible and the test doesn't reuse an old code.

For notifications without a code, check the intended recipient and content, then the action the message supports where relevant. A delivery alert and a login OTP need different final assertions.

Assign a private number to the test account

Provision a MailSlurp phone number for the region and number capabilities your sending provider uses. Keep its displayed number and its MailSlurp ID together: the application sends to the phone number, while your receive API call uses the phone number ID.

Choose the number deliberately rather than taking the first item returned by an account-wide list. That list can contain numbers used by another test or team. For parallel work, assign a separate controlled number to each worker, or serialize tests that share a number.

Phone numbers have a rental lifecycle. The phone documentation explains the required phone plan, API-creation setting and regional variants. Reusing an assigned test number with fresh-message filters is often more practical than allocating a new number on every test attempt. Don't assume that deleting messages releases the number rental or removes the phone from your application's user account.

A number that receives one kind of SMS doesn't establish that every sender route will behave identically. Exercise your application's normal provider and message type, including short-code traffic when that's part of the real journey.

Follow one login from request to account

You can first run this sequence manually in the MailSlurp dashboard, then automate it with Playwright, Cypress or an API test.

Trigger the application's own message

Create or select an owned test account using the assigned number. Capture the request time just before submitting the signup, login or recovery form. Let the application generate the code and send it through its normal provider.

Sending an unrelated text directly to the number is useful as a transport check, but it leaves the application's code generation and sending logic untested. Keep that distinction when diagnosing a failure.

Wait for the correct fresh SMS

Use a bounded wait scoped to the phone number ID and the request time. The MailSlurp waitForLatestSms request supports phoneNumberId, since, timeout and unreadOnly within waitForSingleSmsOptions. The receive-SMS API guide explains the receive methods and when a webhook is more appropriate.

An unread message can still be old. Check the sender, timestamp and identifying content your application supplies before accepting its code. If several messages can arrive at the same number, use the appropriate matching wait or retrieve and inspect the candidates instead of assuming the newest message belongs to this test.

Set a timeout long enough for the delivery route you are testing, with an overall test timeout that also allows the browser steps and cleanup. A delivery timeout should remain a failed delivery expectation. An invalid API key, exhausted allowance or failed request needs its own diagnosis.

Submit the code and verify the account

Extract the code from the received message using the application's actual format. Keep it as text: converting 004281 into a number would remove the leading zeros.

Enter it into the application and submit the verification form. Then assert the expected signed-in user, verified account or completed transaction. A success toast alone can be weaker evidence than the account screen or authenticated response that follows it.

The Playwright SMS OTP guide follows the browser flow. If your suite uses Cypress, start with the Cypress SMS guide. Keep provider setup in those integration steps rather than making every test invent its own receive loop.

Run the OTP policy example locally

The SMS OTP policy example gives you a small runnable application and seven tests for the verification behaviour. Unzip it and run it with Node.js 22 or newer:

cd sms-otp-policy
npm test

No dependency installation or API key is needed. The application runs on a local address, captures its outgoing SMS in memory and exposes challenge, verification and authenticated-account endpoints. The tests retrieve a captured message, submit its code through the verification endpoint and check the exact account returned by the protected /me endpoint.

This local suite covers application policy. It does not send carrier traffic or measure live SMS delivery. For an end-to-end staging test, replace the capture with your real sender and MailSlurp receive path, then keep the same final account assertion.

The example deliberately uses a one-minute expiry, a five-attempt limit and a resend policy that invalidates the earlier challenge. Those are fixture choices, not universal OTP rules. Adapt the expectations to your application's policy.

Test the awkward cases as complete journeys

Resend without accepting the previous code

Request and receive the first code, then request a replacement. If your policy invalidates previous codes, try the old one and confirm rejection. Next, submit the replacement and prove that login succeeds.

The local example checks both the old challenge and an old code submitted against the new challenge. This catches a mistake that a second happy-path login would miss.

Some applications keep a code valid across a resend. In that case, test that documented policy rather than changing the test merely to make every application look like the fixture.

Expiry followed by recovery

A rejected expired code is only half the case. Request a fresh code afterward and confirm the user can still complete login.

The local example advances an injected application clock, so it can exercise expiry without sleeping for a minute. In staging, use the application's supported test-clock mechanism if available, or wait for its real expiry with a bounded test duration. Keep any test-only clock controls out of production.

One-time use and attempt limits

After successful verification, try the consumed challenge from an unauthenticated context and check that it cannot create another session. Also test your configured failed-attempt limit and the application's supported recovery path.

Don't accept an existing signed-in browser session as proof that replay worked or failed. Check the response to the new verification attempt and the account state it actually authorizes.

Parallel account isolation

Run two distinct accounts concurrently and assert each final identity. Separate number allocation, application users and receive filters prevent a passing test from consuming another worker's code.

The local example issues concurrent challenges to two fixture accounts and rejects swapped challenges before signing each account in. A live version also needs separate assigned numbers or another verified delivery-isolation strategy.

If the verification code never arrived

Follow the message through each boundary rather than repeatedly requesting more codes:

  1. Application request: did the form or API accept the expected phone number and trigger a send?
  2. Sending provider: was the request accepted, rejected or delayed? Inspect the actual result rather than a generic "code sent" UI message.
  3. Assigned number: is this the controlled number for the run, with the intended country and capabilities?
  4. Receipt: is the SMS absent, or is the test filtering out a message that arrived? Compare phone ID, timestamp, sender and matching rule.
  5. Verification: if the SMS arrived, was its code still current when submitted, and did it belong to this account?

The failed-send test in the download returns an error and proves that no verified session can be created from the unsent challenge. For a real provider incident, preserve the request time, provider message identifier, phone ID and error result. Keep OTPs and full message bodies out of shared logs.

Clean up the right resources

Remove or reset the application's test user through its supported test-data workflow. Separately decide whether to retain messages for debugging and whether a rented number is intentionally assigned to the next run.

A teardown should only touch resources owned by that test. Deleting every SMS or releasing a shared number can break another worker and make the next failure hard to explain.

Common questions

Does receiving an SMS prove that login works?

Receipt establishes that the text arrived. Submit the current code and assert the authenticated account to prove the login journey.

Can I test without a physical phone?

Yes. A MailSlurp phone number receives messages that your tests can read through the API. Use the dashboard for manual inspection and the same number's API ID in automation.

Should I test expiry locally or through a real provider?

Use fast local tests for application policy, including expiry and attempt limits. Keep a separate end-to-end test through the real sending route for receipt and final verification. Both checks are useful because they exercise different failure points.

Set up a MailSlurp test phone number, complete one successful login, and then add the resend and expiry cases around it.