🤖 App Automation Testing Essential: Automate SMS Verification with Temporary Number Scripts
A battle‑tested guide from a QA engineer who’s tired of typing codes by hand. We’ll wrap a disposable number API into reliable Appium scripts so your regression runs never stall again.
The Pain: That One Manual Step That Destroys Automation
Picture this: your nightly regression suite runs perfectly until it reaches the registration flow. Suddenly the script stops and waits for a human to read a verification code from a physical phone. You’ve tried writing a fixed code into the app (fragile, breaks every build), or begged the backend team to expose a test API (they always say no). The SMS gateways maintained by your company are locked behind firewalls and cost a fortune to use just for testing.
That’s why I started using temporary number APIs within my Appium scripts. It’s a “wild” solution, but it works — and when you’re running 300 tests at 3 AM, all you care about is a green pipeline.
Three Ways to Grab a Verification Code — And Why I Chose “Wild”
| Approach | How it works | Pros | Cons |
|---|---|---|---|
| A. Intercept internal SMS gateway | Ask ops to configure a webhook or mock service that forwards messages to your test agent. | Reliable, no external dependencies | Requires cross‑team coordination; often blocked by security policies |
| B. Virtual number services (Twilio, Plivo) | Buy a long‑term number and receive SMS via webhook API. Pay per month + per message. | Stable, excellent deliverability | Not free; setup overhead; numbers may be flagged by some apps |
| C. Free / cheap disposable number platforms | Use a public or semi‑public API that provides temporary phone numbers and returns SMS content. | Zero cost, instant number provisioning, thousands of numbers | Number quality varies; some numbers already blocked; API may be flaky |
For test automation, I pick Plan C every time. Why? Because a test environment doesn’t need a long‑lived number. It just needs a number that works for the 90 seconds it takes to register. When the test finishes, the number can go back into the pool. And the cost is $0.00 — which makes managers smile.
📊 The Complete Automated Flow — Illustrated
💻 Core Implementation: The TempPhoneService Wrapper
First, we need a Python class that talks to a disposable number API. I’ll use a fictional public API (replace it with the provider you find). The interface is what matters.
import requests
import time
import re
import logging
from tenacity import retry, stop_after_attempt, wait_fixed
logger = logging.getLogger(__name__)
class SMSCodeTimeoutError(Exception):
pass
class TempPhoneService:
def __init__(self, api_key=None):
self.api_key = api_key or 'free-key'
self.base_url = 'https://api.example-sms-provider.com'
self.current_number = None
self.number_id = None
@retry(stop=stop_after_attempt(3), wait=wait_fixed(5))
def get_number(self, country='US', service='app'):
"""Request a temporary number. Some services allow filtering by 'service'."""
payload = {'country': country, 'service': service, 'apikey': self.api_key}
resp = requests.get(f'{self.base_url}/get-number', params=payload, timeout=10)
data = resp.json()
if data.get('status') != 'success':
raise RuntimeError(f"Failed to get number: {data}")
self.current_number = data['phone_number']
self.number_id = data['id']
logger.info(f"Obtained number: {self.current_number} (id: {self.number_id})")
return self.current_number
@retry(stop=stop_after_attempt(3), wait=wait_fixed(5))
def skip_number(self):
"""Mark a number as unusable and get a new one."""
if self.number_id:
requests.post(f'{self.base_url}/skip-number', json={'id': self.number_id, 'apikey': self.api_key})
self.current_number = None
self.number_id = None
return self.get_number() # recursive with retry
def wait_for_sms(self, max_wait=60, poll_interval=3):
"""Poll for SMS content until timeout. Returns the full SMS body, or raises."""
deadline = time.time() + max_wait
while time.time() < deadline:
try:
resp = requests.get(
f'{self.base_url}/get-sms',
params={'id': self.number_id, 'apikey': self.api_key},
timeout=10
)
data = resp.json()
if data.get('status') == 'success' and data.get('sms_list'):
# Sort by time to get the latest
latest = sorted(data['sms_list'], key=lambda x: x.get('time', ''), reverse=True)[0]
body = latest['text']
sender = latest.get('from', '')
logger.info(f"SMS received from {sender}: {body}")
return body, sender
except Exception as e:
logger.warning(f"Polling error: {e}")
time.sleep(poll_interval)
raise SMSCodeTimeoutError(f"No SMS received for {self.current_number} within {max_wait}s")
def release_number(self):
if self.number_id:
requests.post(f'{self.base_url}/release-number', json={'id': self.number_id, 'apikey': self.api_key})
logger.info(f"Released number {self.current_number}")
Note the tenacity retry decorators. Those save us when the free API randomly returns 503. Also, skip_number is essential — if the app silently blocks the number, we immediately discard it and grab a fresh one.
🔌 Integrating with Appium Page Objects
We’ll extend the registration page object to accept a temporary number service instance. The method register_with_temp_phone encapsulates the entire flow.
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import re
class RegistrationPage:
def __init__(self, driver):
self.driver = driver
self.phone_input = (AppiumBy.ID, 'phoneNumberField')
self.send_code_btn = (AppiumBy.ID, 'sendCodeButton')
self.code_input = (AppiumBy.ID, 'verificationCodeField')
self.verify_btn = (AppiumBy.ID, 'verifyButton')
self.success_lbl = (AppiumBy.ID, 'registrationSuccess')
def register_with_temp_phone(self, temp_service: TempPhoneService, country_code='US'):
# 1. Get a fresh number
phone = temp_service.get_number(country=country_code)
# 2. Enter phone in app
self.driver.find_element(*self.phone_input).send_keys(phone)
self.driver.find_element(*self.send_code_btn).click()
# 3. Wait for verification code
try:
sms_body, sender = temp_service.wait_for_sms(max_wait=60)
except SMSCodeTimeoutError:
# Capture screenshot and skip / raise
self.driver.save_screenshot('sms_timeout.png')
temp_service.skip_number()
raise
# 4. Extract 4‑6 digit code
code_match = re.search(r'\b(\d{4,6})\b', sms_body)
if not code_match:
raise ValueError(f"No verification code found in: {sms_body}")
code = code_match.group(1)
# 5. Enter code and verify
self.driver.find_element(*self.code_input).send_keys(code)
self.driver.find_element(*self.verify_btn).click()
# 6. Wait for success
WebDriverWait(self.driver, 15).until(
EC.visibility_of_element_located(self.success_lbl)
)
# 7. Release number (cleanup)
temp_service.release_number()
return phone
The page object method is now a one‑call command: no outside coordination needed.
🧪 A Complete Pytest Test Case
import pytest
from temp_phone_service import TempPhoneService
from pages.registration_page import RegistrationPage
@pytest.fixture(scope='function')
def temp_phone():
service = TempPhoneService(api_key='my-test-key')
yield service
# Best effort cleanup
try:
service.release_number()
except:
pass
def test_user_registration_with_temporary_number(driver, temp_phone):
reg_page = RegistrationPage(driver)
try:
phone = reg_page.register_with_temp_phone(temp_phone, country_code='US')
# Additional assertions (e.g., welcome page, database)
print(f"✅ Registered successfully with {phone}")
except Exception:
driver.save_screenshot('registration_failure.png')
raise
💣 The Four Pitfalls I’ve Stumbled Into (So You Don’t)
Many temporary numbers are known to popular apps and blacklisted. The app never sends an SMS. Solution: the skip_number() method combined with a retry loop in the test (e.g., attempt up to 3 different numbers).
A reused number may receive dozens of old messages. If you just grab the latest without filtering by sender or time window, you might insert a stale code. Fix: check the timestamp returned by the API and only accept messages that arrived after you clicked “Send Code”.
503 errors, empty responses, or connection timeouts happen regularly. That’s why we wrapped every API call with tenacity retries. Also set a global request timeout (5‑10 seconds) so your test doesn’t hang forever.
Some services maintain their own database of virtual numbers and refuse to send SMS to them. When you hit this, the API‑based temporary number approach fails entirely. Fallback options: switch to a different provider, use a real SIM bank (like the hardware module solution we covered earlier), or mock the SMS verification in your testing environment if possible.
🚀 Running in CI/CD Pipelines
When you move these scripts to Jenkins, GitHub Actions, or GitLab CI, keep these rules:
- Never hardcode API keys. Use environment variables (
os.environ['SMS_API_KEY']) and inject them via the CI secrets store. - Store screenshots and logs as artifacts. When a test fails, you want the screenshot and the polling log to debug.
- Isolate numbers across parallel runs. If you execute the same test in 5 parallel threads, they might accidentally share a number if the API doesn’t provide uniqueness. Use a unique test run ID or different country codes to partition the pool.
⚖️ A Quick Word on Responsibility
Temporary number services are powerful, but they walk a fine line. I use them exclusively for internal test automation on applications my team owns. Using them to bypass registration limits on third‑party services or to create fake accounts is almost certainly against the terms of service and may have legal consequences. Keep your automation ethical and within the boundaries of the software you’re authorized to test.
🏁 Wrapping Up
You now have a complete, runnable pattern for automating SMS verification with temporary numbers. The core takeaways: wrap the flaky API in retries, always have a skip‑and‑retry fallback for dead numbers, and integrate the entire flow into a single page object method so your tests stay clean and readable. Your regression suite can finally cross that registration hurdle without human hands.
Go ahead, plug it into your Appium project tonight, and watch those verification codes pour into your logs like magic.