+1

Nhận SMS OTP tự động qua API: code Python cho automation test

Nhận SMS OTP tự động qua API là cách để script test tự lấy mã xác minh mà không cần điện thoại thật: gọi API thuê số ảo, điền số vào luồng đăng ký, rồi poll để đọc mã trả về. Bài này mô tả luồng bốn bước, code Python chạy được và những chỗ hay làm test flaky.

Khi nào cần API nhận SMS OTP (và khi nào không cần)

Ai làm QA hoặc automation chắc từng gặp bài toán này: cần test flow đăng ký có xác minh SMS, mà mỗi lần chạy test lại phải có số điện thoại mới. Dùng số cá nhân thì được vài lần là hết cửa, mua SIM vật lý thì vừa đắt vừa không scale được khi cần chạy song song.

Nhưng không phải lúc nào cũng cần. Nếu bạn test hệ thống của chính mình, cách rẻ nhất là xin backend một chế độ test: cố định mã OTP trong môi trường staging, hoặc mở endpoint nội bộ trả về mã vừa gửi. Số ảo chỉ thực sự cần khi bạn không kiểm soát được đầu gửi SMS, ví dụ test tích hợp với dịch vụ bên thứ ba, hoặc kiểm tra xem luồng đăng ký có hoạt động với đầu số của từng quốc gia hay không.

Số ảo hoạt động thế nào

Dịch vụ số ảo cho thuê số điện thoại theo phiên: bạn chọn quốc gia và ứng dụng đích, hệ thống cấp một số, SMS gửi đến số đó được đọc qua dashboard hoặc API. Số thường dùng một lần cho một dịch vụ, xong phiên thì trả lại pool.

Luồng hoạt động: 4 bước nhận mã OTP tự động qua API

Flow cơ bản qua API thường gồm bốn bước:

  1. Request số mới cho service cần test, nhận về id phiên và số điện thoại
  2. Điền số vào form đăng ký của ứng dụng đang test
  3. Poll endpoint đọc OTP đến khi có mã
  4. Xác minh xong thì đóng phiên

Các nhà cung cấp phổ biến ở thị trường Việt Nam như HeroSMS, 5sim hay SMS-Activate đều đi theo mô hình này, chỉ khác nhau ở tên endpoint và cách trả dữ liệu. Đọc kỹ tài liệu của bên bạn chọn trước khi code, vì có bên trả JSON, có bên trả plain text kiểu ACCESS_NUMBER:....

Ví dụ Python: thuê số và poll mã OTP

Đoạn dưới viết theo tài liệu REST của HeroSMS (server https://hero-sms.com/api/v1, xác thực bằng header Authorization: ApiKey <token>). Nếu dùng nhà cung cấp khác, chỉ cần sửa hai hàm thue_sodoc_otp.

import time
import requests

API = "https://hero-sms.com/api/v1"
KEY = "YOUR_API_KEY"
HEADERS = {"Authorization": f"ApiKey {KEY}"}


def thue_so(service="tg", country=2, verification_type="sms"):
    """Mua một activation. service: mã dịch vụ 2-4 ký tự (tg, fb, ig...).
    country: ID quốc gia dạng số. Trả về (activation_id, so_dien_thoai)."""
    r = requests.post(
        f"{API}/activations",
        headers=HEADERS,
        json={
            "service": service,
            "country": country,
            "amount": 1,
            "verificationType": verification_type,
        },
        timeout=30,
    )
    r.raise_for_status()
    act = r.json()["data"][0]
    return act["id"], act["phone"]


def doc_otp(activation_id):
    """Đọc OTP mới nhất. Chưa có mã thì smsCode là null."""
    r = requests.get(
        f"{API}/activations/{activation_id}/otp/last",
        headers=HEADERS,
        timeout=30,
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return (r.json().get("data") or {}).get("smsCode")


def cho_ma_otp(activation_id, timeout=180, interval=5):
    """SMS quốc tế thường về sau 30-120 giây, nên để timeout 180s."""
    het_han = time.time() + timeout
    while time.time() < het_han:
        code = doc_otp(activation_id)
        if code:
            return code
        time.sleep(interval)
    raise TimeoutError(f"Khong nhan duoc OTP trong {timeout}s")


def dong_phien(activation_id, thanh_cong=True):
    """Xong việc thì đóng phiên: finish nếu đã dùng mã, cancel nếu bỏ."""
    if thanh_cong:
        requests.post(f"{API}/activations/{activation_id}/finish",
                      headers=HEADERS, timeout=30)
    else:
        requests.delete(f"{API}/activations/{activation_id}",
                        headers=HEADERS, timeout=30)

Dùng thực tế:

activation_id, phone = thue_so(service="tg", country=2)
try:
    # điền phone vào form đăng ký ở đây
    code = cho_ma_otp(activation_id)
    print("Ma OTP:", code)
    dong_phien(activation_id, thanh_cong=True)
except TimeoutError:
    dong_phien(activation_id, thanh_cong=False)
    raise

Tích hợp vào Selenium và Playwright

Phần script trình duyệt chỉ cần hai chỗ: điền số và điền mã.

# Playwright
page.fill("#phone", phone)
page.click("#send-otp")
page.fill("#otp", cho_ma_otp(activation_id))

# Selenium
driver.find_element(By.ID, "phone").send_keys(phone)
driver.find_element(By.ID, "send-otp").click()
driver.find_element(By.ID, "otp").send_keys(cho_ma_otp(activation_id))

Nên bọc thue_sodong_phien vào fixture của pytest, để phiên luôn được đóng kể cả khi test fail giữa chừng.

Polling hay webhook: chọn cách nào

Tiêu chí Polling Webhook
Độ trễ nhận code 1 chu kỳ poll (3-5 giây) gần như tức thì
Độ phức tạp thấp, chạy từ máy local cần endpoint public
Phù hợp với test local, số lượng ít CI/CD, chạy song song nhiều luồng
Chi phí request tốn nhiều request 1 request cho 1 SMS
Rủi ro chính timeout đặt sai làm test flaky mất SMS nếu endpoint down

Nếu chạy vài chục case một ngày thì polling là đủ. Khi số luồng song song tăng lên, poll 5 giây một lần cho mỗi luồng bắt đầu tốn request vô ích, lúc đó mới cần webhook.

Timeout và retry: SMS quốc tế mất 30-120 giây

Timeout nên đặt thoáng, tối thiểu 2-3 phút cho số quốc tế. Retry logic nên phân biệt hai trường hợp khác nhau: "chưa có mã" thì chờ tiếp, còn "số bị dịch vụ đích từ chối" thì hủy phiên và xin số mới, chờ thêm cũng vô ích. Gộp chung hai case này là nguyên nhân phổ biến nhất làm test vừa chậm vừa flaky.

Một điểm nữa hay bị bỏ qua: nếu ứng dụng đích gửi mã qua cuộc gọi thay vì SMS ở một số quốc gia, hãy chọn đúng verificationType khi mua activation, nếu không sẽ chờ mãi một tin nhắn không bao giờ đến.

Chạy test song song cần bao nhiêu số điện thoại ảo

Mỗi luồng test cần một số riêng, nên số phiên đồng thời bằng đúng số worker. Với pytest-xdist chạy 8 worker thì cần 8 phiên song song, và pool số của quốc gia đó phải đủ. Đây cũng là lý do nên log lại tỷ lệ thành công theo từng quốc gia và dịch vụ: cùng một ứng dụng nhưng số của nước khác nhau cho kết quả khác hẳn nhau, sau vài trăm phiên bạn sẽ biết nên chọn quốc gia nào cho suite của mình.

Tiêu chí chọn nhà cung cấp cho mục đích kỹ thuật cũng khác người dùng cuối: độ trễ nhận SMS ảnh hưởng trực tiếp thời gian chạy test, tỷ lệ số "sạch" chưa bị dịch vụ đích chặn quyết định độ ổn định, và chính sách hoàn tiền khi SMS không về giúp CI không đốt tiền vào những lần fail ngoài tầm kiểm soát.

Ranh giới sử dụng

Số ảo hợp lý cho việc test luồng đăng ký, tách biệt môi trường staging với số cá nhân, và kiểm tra khả năng tương thích với đầu số từng quốc gia. Dùng để tạo tài khoản hàng loạt đi spam người khác thì vừa vi phạm ToS các nền tảng vừa làm bẩn pool số chung, và tài khoản dựng bằng số thuê cũng sẽ mất khi dịch vụ đích bắt xác minh lại, vì số đó không còn thuộc về bạn. Dùng đúng việc thì công cụ bền, mình cũng đỡ đau đầu.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.