SMS and OTP Verification: How to Automate Flows

Every test engineer runs into the same wall sooner or later: an automated signup suite that works perfectly until the application demands a one-time password delivered to an actual handset.

Teams usually solve it by wiring a virtual line into the test environment, and for messenger-driven signups, a temporary WhatsApp number receives the code just as reliably as a SIM card sitting in someone’s drawer.

This article explains how OTP verification works underneath, why it breaks automated suites so consistently, and the four approaches teams use to get their tests green again without weakening the security of the feature they are testing.

How OTP Verification Actually Works Under the Hood

A one-time password is a short lived secret generated by the server, delivered over a channel the user already controls, and accepted exactly once. Most implementations fall into two families.

The first is server-generated random codes. When the user submits a phone number, the backend creates a random value, stores it with a timestamp and an attempt counter, and hands it to an SMS gateway or a messaging API. Validation is a lookup: correct value, not expired, attempts not exhausted.

The second family is time-based codes, the TOTP scheme behind authenticator apps. The server and client share a secret, and both derive the same code from the current time window, which means nothing has to travel over SMS at all. TOTP is the stronger option, but consumer products keep using SMS and messenger delivery because users already have a phone and nobody has to install anything.

For a tester, the practical difference is enormous. A TOTP code can be generated inside the test itself from the shared secret, using any standard library. An SMS or messenger code exists only after a real message reaches a real destination, which is exactly what an automated pipeline cannot conjure on its own.

Why Automated Tests Break on SMS and OTP Verification

SMS verification looks trivial from the outside, yet four properties of these flows fight against automation at once.

Delivery is asynchronous and not instant. A code may arrive in two seconds or in forty, depending on the carrier and the route, so a fixed sleep either wastes minutes per run or fails intermittently. Flakiness that appears only in CI almost always traces back to a hard-coded wait around a verification step.

The code lives outside the system under test. Your Selenium session drives a browser; the secret arrives in a completely separate inbox. Without a programmable way to read that inbox, the chain is broken.

Numbers get consumed. Many applications allow one account per number, so a suite that registers a fresh user on every run needs a fresh line on every run or a strategy for reusing one.

Rate limits and anti-fraud systems watch. Request twenty codes for the same number in an hour, and the provider, the application, or both will start rejecting you. Tests that ignore this pass in the morning and fail after lunch for reasons nobody can reproduce locally.

Four Ways Teams Handle OTP in Automated Test Suites

Mocking the gateway is the cheapest. In a test profile, replace the SMS provider with a stub that records the code instead of sending it. Fast, free, and completely deterministic. The weakness is coverage: you are no longer testing the integration that most often breaks in production, which is delivery itself.

Exposing a test only hook goes one step further. A protected endpoint returns the latest code for a given number when a secret header is present, or the backend writes a fixed code such as 000000 for accounts flagged as test users.

Nearly as fast as mocking and closer to the real path. It must be strictly disabled in production builds; a forgotten hook is a full authentication bypass, and this has burned real companies.

Reading a real inbox programmatically is the only option that proves the whole chain. The test requests the code, then polls an API that exposes messages delivered to a dedicated line. This exercises the genuine production path end to end, including the gateway and the template. It is slower and depends on an external service, so it belongs in a nightly or pre-release suite rather than in every commit build.

Keeping a pool of long-lived accounts avoids the problem instead of solving it. Register a set of verified test accounts once, store their sessions, and skip registration in most runs. Registration itself then gets covered by a small dedicated suite instead of by every scenario.
Mature teams combine them: mocks in the fast feedback loop and real numbers in the nightly run.

Where a Virtual Number for WhatsApp or SMS Fits into a Test Environment

A virtual number is an ordinary working phone line hosted by a telecom provider rather than tied to a physical SIM. Messages arrive in a dashboard or, more usefully for engineers, through an API that a test can query. This is what makes a virtual number for WhatsApp or SMS practical in automation: the inbox is addressable from code.

The setup is straightforward. Take a line confirmed to work with the channel you are testing, register the test account on it, store the number and credentials in your secret manager next to everything else, and give the suite a helper that polls for the newest message. Because the line persists, the same account can be reused across runs instead of burning a new registration every time.

Three details are worth deciding upfront. Choose a line in the country whose users you actually serve, since delivery routes and templates differ by region. Use a rented line rather than a disposable one for anything permanent, because a recycled number takes your test account with it. And keep the number dedicated to testing, never shared with a colleague’s personal phone.

Using a Temporary WhatsApp Number for Messenger-Based OTP Flows

Messenger delivery has become the default in markets where data is cheaper than SMS, and India is the clearest example. Applications increasingly send verification templates through the WhatsApp Business API instead of, or alongside, a text message, which means QA has to cover that path too.

The mechanics do not change much. A temporary WhatsApp number works the same way as an SMS line: the account is registered against it, the template arrives, and the test extracts the code.

What does change is the failure modes you should be probing. Template approval status, session windows, and fallback to SMS when the message cannot be delivered are all worth explicit test cases, because they fail silently in a way plain SMS does not.

One practical warning: pick the number type according to how long the account must live. A temporary number for WhatsApp taken for an hour, or a temp number for WhatsApp rented for a single run, is fine for a throwaway scenario, but if the application later asks that account to re-verify and the line has been recycled, the account is unrecoverable. For a permanent test account, rent the line for as long as the account is expected to exist.

Writing the Selenium Step that Waits for the Code

The core of the implementation is a poll with a timeout, not a sleep. Using FluentWait keeps it readable and removes the guesswork:

public String waitForOtp(SmsClient client, String number) {
     return new FluentWait<>(client)
    .withTimeout(Duration.ofSeconds(90))
    .pollingEvery(Duration.ofSeconds(5))
    .ignoring(CodeNotReceivedException.class)
    .until(c -> c.latestCode(number));
}

The verification step in the scenario then stays clean:

driver.findElement(By.id("phone")).sendKeys(testNumber);
driver.findElement(By.id("send-code")).click();
String code = waitForOtp(smsClient, testNumber);
driver.findElement(By.id("otp")).sendKeys(code);
driver.findElement(By.id("verify")).click();

Two rules keep this stable. Record the timestamp before requesting the code, and accept only messages newer than that moment; otherwise, a leftover code from a previous run will be picked up, and the test will fail with a misleading message. And extract the digits with an explicit pattern rather than trusting the message layout, since providers change template wording without warning.

Security and Hygiene When Tests Use Real Numbers

Treat the line as a credential. Numbers and API keys belong in a secret store, not in a repository, because anyone who can read that inbox can take over every account registered on it.

Restrict the provider API key to the permissions the suite actually needs, keep test data out of production databases, and rotate the line if it ever appears in a public log.

It is also worth asserting that your production build genuinely rejects the test hook: a quick negative test costs nothing and catches the single most dangerous mistake in this whole area.

Choosing Between a Mock, a Test Hook, and a Real Number

There is no universal answer, only a sensible division of labour. Mocks belong in the commit pipeline where speed decides everything. A guarded test hook suits staging, where you want the real backend without the latency of delivery.

Real lines, including a temporary WhatsApp number for messenger flows, belong in the pre-release suite that has to prove the whole chain works before users meet it.

Pick the number type to match how long the test account must survive, poll instead of sleeping, and filter by timestamp, and OTP verification stops being the step that makes your pipeline unreliable.

DEEPAK GUPTA

DEEPAK GUPTA

Deepak Gupta is the Founder of Scientech Easy, a Full Stack Developer, and a passionate coding educator with 8+ years of professional experience in Java, Python, web development, and core computer science subjects. With strong expertise in full-stack development, he provides hands-on training in programming languages and in-demand technologies at the Scientech Easy Institute, Dhanbad.

He regularly publishes in-depth tutorials, practical coding examples, and high-quality learning resources for both beginners and working professionals. Every article is carefully researched, technically reviewed, and regularly updated to ensure accuracy, clarity, and real-world relevance, helping learners build job-ready skills with confidence.