Route your app's SMS to a mock inbox in the test environment. In the Selenium test, use a unique phone number, note the time, trigger the SMS, then poll the inbox's HTTP API for a code sent to that number after that time and type it into the page. It works the same in Python, Java or any other binding.
Step 1: route SMS to the mock
Your application, not the test, sends the SMS. In the test or staging environment, point your provider's SDK at otpmock with one option. Each provider's option is on its provider page; otpmock emulates fifteen, including Twilio, Vonage, AWS SNS, Infobip, Sinch and Plivo.
Step 2: the inbox API your test calls
One request returns the newest code sent to a number after a timestamp, or 404 if none has arrived yet:
GET https://api.otpmock.com/v1/inbox/{phone}/code?since={unix_ms}
Authorization: Bearer $OTPMOCK_API_KEY
200 {"code":"482913","messageSid":"SM…","body":"Your code is 482913","receivedAt":1791417652342}Python
import os, random, time, urllib.parse
import requests
API = os.environ.get("OTPMOCK_URL", "https://api.otpmock.com")
KEY = os.environ["OTPMOCK_API_KEY"]
def random_phone():
return "+1555" + "".join(random.choices("0123456789", k=7))
def wait_for_code(phone, since_ms, timeout=15):
url = f"{API}/v1/inbox/{urllib.parse.quote(phone)}/code"
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(url, params={"since": since_ms}, headers={"Authorization": f"Bearer {KEY}"})
if r.status_code == 200:
return r.json()["code"]
if r.status_code != 404:
r.raise_for_status()
time.sleep(0.3)
raise TimeoutError(f"no code for {phone}")from selenium.webdriver.common.by import By
from otp import random_phone, wait_for_code
import time
def test_phone_signup(driver):
phone = random_phone()
driver.get("https://staging.example.com/signup")
driver.find_element(By.NAME, "phone").send_keys(phone)
since = int(time.time() * 1000) - 5000 # small margin for clock skew
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
code = wait_for_code(phone, since)
driver.find_element(By.NAME, "code").send_keys(code)
driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()Java
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.util.regex.*;
public final class OtpMock {
private static final HttpClient HTTP = HttpClient.newHttpClient();
private static final String API = System.getenv().getOrDefault("OTPMOCK_URL", "https://api.otpmock.com");
private static final String KEY = System.getenv("OTPMOCK_API_KEY");
public static String waitForCode(String phone, long sinceMs) throws Exception {
String url = API + "/v1/inbox/" + URLEncoder.encode(phone, StandardCharsets.UTF_8) + "/code?since=" + sinceMs;
long deadline = System.currentTimeMillis() + 15_000;
while (System.currentTimeMillis() < deadline) {
HttpResponse<String> r = HTTP.send(
HttpRequest.newBuilder(URI.create(url)).header("Authorization", "Bearer " + KEY).build(),
HttpResponse.BodyHandlers.ofString());
if (r.statusCode() == 200) {
Matcher m = Pattern.compile("\"code\":\"([^\"]+)\"").matcher(r.body());
if (m.find()) return m.group(1);
} else if (r.statusCode() != 404) {
throw new IllegalStateException("otpmock " + r.statusCode() + ": " + r.body());
}
Thread.sleep(300);
}
throw new IllegalStateException("no code for " + phone);
}
}Use your JSON library of choice instead of the regex if one is already on the classpath.
Selenium Grid and parallel runs
Each test generates its own number, so parallel sessions never read each other's codes. The test runner only needs outbound HTTPS to api.otpmock.com; the browser nodes don't talk to otpmock at all.
Common pitfalls
- Setting the variable only on the test runner. The app server sends the SMS, so the otpmock option must be set where the app runs.
- Formatting differences. If your UI adds a country code or strips the
+, query the number in the form the app sent. otpmock stores numbers as+and digits. - Waiting with
time.sleep. Poll the inbox instead of sleeping a fixed time; codes usually arrive in well under a second.
FAQ
Does this work with Selenium IDE or other languages?
Any test that can make an HTTP request can read codes. For C#, Ruby or JavaScript bindings, port the polling loop above; it is a single GET request.
Do I need to change application code?
Only where the SMS client is created, behind an environment variable that is set in test environments.
The free plan includes 100 messages a month. No card required.
Get a free API key