CAPTCHA không phân biệt bot thật với test runner của chính bạn — sitekey .g-recaptcha trên trang login chặn Selenium y hệt cách nó chặn kẻ tấn công. Cách xử lý đúng không phải là tắt CAPTCHA ở môi trường staging (nhiều đội làm vậy rồi quên bật lại), mà là giải nó qua API ngay trong pipeline. Bài này đi thẳng vào cách nối CaptchaAI vào Pytest, Selenium E2E và CI, cùng cách giữ chi phí ở mức chấp nhận được khi test chạy hàng trăm lần một ngày.
Ba điều cần nắm trước khi viết code:
- Đây là kiểm thử được ủy quyền trên hệ thống bạn có quyền truy cập hợp pháp — khác hoàn toàn với việc bypass phòng thủ của một bên thứ ba không liên quan.
- Một lần giải reCAPTCHA v2 tốn khoảng $0,003. Với gói BASIC ($15/tháng, 5 thread), một đội QA nhỏ chạy vài nghìn lần giải mỗi tháng vẫn rẻ hơn nhiều so với một giờ công kiểm tra thủ công.
- Cùng một helper class dùng lại được cho Pytest, Selenium và job CI riêng — viết một lần, dùng khắp pipeline.
Khi nào đội QA cần giải CAPTCHA tự động
| Kịch bản | Vì sao cần CaptchaAI |
|---|---|
| Kiểm thử hồi quy | Xác minh biểu mẫu vẫn hoạt động sau mỗi lần triển khai |
| Kiểm thử end-to-end | Kiểm tra toàn bộ hành trình người dùng, kể cả bước có CAPTCHA |
| Kiểm thử tải (load test) | Mô phỏng lượng lớn phiên có CAPTCHA cùng lúc |
| Kiểm thử đa trình duyệt | Xác nhận CAPTCHA render đúng trên từng trình duyệt |
| Kiểm thử khả năng tiếp cận | Kiểm tra luồng thay thế cho người dùng khuyết tật |
Ví dụ thực tế: một đội QA tại công ty gia công phần mềm chạy bộ test hồi quy mỗi đêm cho ứng dụng của khách hàng nước ngoài. Trang đăng nhập của khách hàng dùng reCAPTCHA v2 — nếu không giải tự động, pipeline CI dừng ngay ở bước login và phải chờ người kiểm tra thủ công vào sáng hôm sau, trễ mất nửa ngày phát hiện lỗi.
Viết helper giải CAPTCHA dùng chung cho Pytest
Đặt logic gửi task và polling kết quả vào một class dùng lại được, thay vì rải rác trong từng file test:
import pytest
import requests
import time
class CaptchaTestHelper:
"""Helper for solving CAPTCHAs in test environments."""
def __init__(self, api_key):
self.api_key = api_key
def solve_recaptcha(self, sitekey, pageurl, timeout=120):
"""Solve reCAPTCHA and return token."""
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1,
}, timeout=30)
result = resp.json()
assert result.get("status") == 1, f"Submit failed: {result}"
task_id = result["request"]
deadline = time.time() + timeout
time.sleep(10)
while time.time() < deadline:
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key, "action": "get",
"id": task_id, "json": 1,
}, timeout=15)
data = resp.json()
if data.get("status") == 1:
return data["request"]
if data["request"] != "CAPCHA_NOT_READY":
raise RuntimeError(f"Solve error: {data['request']}")
time.sleep(5)
raise TimeoutError("CAPTCHA solve timeout")
@pytest.fixture(scope="session")
def captcha_helper():
"""Provide CaptchaAI helper for test session."""
import os
api_key = os.environ.get("CAPTCHAAI_API_KEY")
if not api_key:
pytest.skip("CAPTCHAAI_API_KEY not set")
return CaptchaTestHelper(api_key)
class TestLoginFlow:
"""Test login flow behind reCAPTCHA."""
SITEKEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
LOGIN_URL = "https://staging.example.com/qa-login"
def test_login_with_valid_credentials(self, captcha_helper):
"""Verify login succeeds with valid creds and solved CAPTCHA."""
token = captcha_helper.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
assert token and len(token) > 100
resp = requests.post(self.LOGIN_URL, data={
"username": "test_user",
"password": "test_pass",
"g-recaptcha-response": token,
})
assert resp.status_code == 200
assert "Welcome" in resp.text
def test_login_with_invalid_credentials(self, captcha_helper):
"""Verify login fails gracefully with bad creds but valid CAPTCHA."""
token = captcha_helper.solve_recaptcha(self.SITEKEY, self.LOGIN_URL)
resp = requests.post(self.LOGIN_URL, data={
"username": "wrong_user",
"password": "wrong_pass",
"g-recaptcha-response": token,
})
assert resp.status_code in (200, 401)
assert "Invalid" in resp.text or "error" in resp.text.lower()
def test_login_without_captcha_fails(self):
"""Verify login rejects submissions without CAPTCHA."""
resp = requests.post(self.LOGIN_URL, data={
"username": "test_user",
"password": "test_pass",
})
assert resp.status_code in (400, 403, 422)
Fixture captcha_helper tự skip test nếu biến môi trường CAPTCHAAI_API_KEY chưa được set, thay vì để cả suite fail vì thiếu cấu hình.
Dùng lại helper trong test Selenium E2E
Test E2E cần điền form thật trên trình duyệt, không chỉ gọi API — sitekey phải được trích từ DOM trước khi gửi cho CaptchaAI:
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
@pytest.fixture
def browser():
"""Create browser for testing."""
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=options)
yield driver
driver.quit()
class TestRegistrationFlow:
"""Test registration form with CAPTCHA."""
REG_URL = "https://staging.example.com/register"
def test_registration_form_submits(self, browser, captcha_helper):
"""Full registration flow with CAPTCHA solving."""
browser.get(self.REG_URL)
# Fill form
browser.find_element(By.ID, "email").send_keys("[email protected]")
browser.find_element(By.ID, "password").send_keys("SecurePass123!")
browser.find_element(By.ID, "confirm_password").send_keys("SecurePass123!")
# Extract sitekey from page
captcha_div = browser.find_element(By.CSS_SELECTOR, ".g-recaptcha")
sitekey = captcha_div.get_attribute("data-sitekey")
# Solve via API
token = captcha_helper.solve_recaptcha(sitekey, browser.current_url)
# Inject token
browser.execute_script("""
document.querySelector('[name="g-recaptcha-response"]').value = arguments[0];
""", token)
# Trigger callback if needed
callback = captcha_div.get_attribute("data-callback")
if callback:
browser.execute_script(f"window['{callback}'](arguments[0]);", token)
# Submit
browser.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
# Verify success
WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".success-message"))
)
def test_captcha_renders_on_page(self, browser):
"""Verify CAPTCHA widget loads on registration page."""
browser.get(self.REG_URL)
captcha = WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, iframe[src*='recaptcha']"))
)
assert captcha.is_displayed()
Đoạn execute_script gọi callback JS của widget nếu trang yêu cầu — nhiều theme reCAPTCHA không tự nhận token nếu bạn chỉ set value bằng DOM mà bỏ qua bước này.
Tách test CAPTCHA khỏi pipeline nhanh bằng marker
Không phải lần push nào cũng cần chạy lại toàn bộ test có CAPTCHA — dùng marker của pytest để tách hai luồng, một luồng nhanh cho mỗi commit và một luồng đầy đủ chạy theo lịch:
# conftest.py
import os
# Mark tests that need CAPTCHA solving
def pytest_configure(config):
config.addinivalue_line(
"markers", "captcha: tests requiring CAPTCHA solving (may be slow)"
)
# pytest.ini or pyproject.toml
"""
[tool.pytest.ini_options]
markers = [
"captcha: tests requiring CAPTCHA solving (may be slow)",
]
"""
Chạy riêng các test có CAPTCHA (job đêm hoặc trước khi merge):
pytest -m captcha -v
Chạy pipeline nhanh, bỏ qua các test CAPTCHA (mỗi lần push):
pytest -m "not captcha" -v
Kiểm soát chi phí khi chạy test CAPTCHA thường xuyên
| Chiến lược | Lợi ích |
|---|---|
| Test trên môi trường staging | CAPTCHA ở staging thường dễ giải hơn ở production |
| Chạy test CAPTCHA theo lịch, không phải mỗi lần push | Giảm số request tới API |
| Cache kết quả cho các test hay bị flaky | Tránh giải lại CAPTCHA không cần thiết |
| Dùng biến môi trường để tắt CAPTCHA khi dev cục bộ | Tiết kiệm chi phí trong lúc code |
| Gom test CAPTCHA vào một CI job riêng | Kiểm soát chi phí theo từng pipeline |
Vì CaptchaAI tính phí theo thread giải đồng thời chứ không theo mỗi lần giải, chạy nhiều test song song không tự động làm tăng chi phí — gói BASIC (5 thread) đã đủ cho phần lớn CI job chạy tuần tự; chỉ cần nâng thread khi bạn thật sự chạy nhiều suite CAPTCHA cùng lúc.
Câu hỏi thường gặp
Chạy test CAPTCHA mỗi ngày tốn bao nhiêu?
Mỗi lần giải reCAPTCHA v2 khoảng $0,003. Một bộ 50 test CAPTCHA tốn khoảng $0,15 mỗi lần chạy; chạy hàng ngày rơi vào khoảng $4,50/tháng — thấp hơn nhiều so với gói BASIC ($15/tháng, 5 thread) mà hầu hết đội QA đã cần cho các use case khác.
Làm sao để test không bị flaky vì CAPTCHA?
Đặt timeout đủ lớn (khuyến nghị 120 giây) trước khi coi là lỗi thật, và đừng retry vô hạn. Sau 2–3 lần thử lại vẫn timeout, để test fail rõ ràng thay vì treo cả pipeline — một CAPTCHA giải chậm bất thường thường là dấu hiệu của vấn đề khác, không phải lý do để retry mãi.
Cần bao nhiêu thread để chạy test CAPTCHA song song trong CI?
CaptchaAI tính phí theo thread giải đồng thời, không theo số lần giải. Một CI job chạy tuần tự chỉ cần 1 thread bận tại một thời điểm, nên BASIC (5 thread) đủ cho hầu hết đội QA nhỏ; chỉ nâng lên STANDARD ($30/tháng, 15 thread) trở lên khi bạn chạy nhiều suite CAPTCHA song song thật sự.
CaptchaAI có giải được Cloudflare Turnstile và GeeTest trong test không?
Có. Cùng mô hình gửi task tới in.php rồi polling res.php áp dụng cho Cloudflare Turnstile, GeeTest v3, CAPTCHA hình ảnh và các loại CaptchaAI hỗ trợ — chỉ khác tham số method khi gửi task. hCaptcha và FunCaptcha thì CaptchaAI chưa hỗ trợ, kể cả trong môi trường test.
Có cần proxy khi giải CAPTCHA trên môi trường staging không?
Thường thì không. Đây là hệ thống bạn sở hữu và có quyền truy cập, nên request kiểm thử chạy trực tiếp từ máy CI mà không cần proxy hay VPN. Chỉ cân nhắc proxy khi luồng bạn kiểm thử thật sự phụ thuộc vào vị trí địa lý, ví dụ nội dung hiển thị theo khu vực.
Tài nguyên liên quan
Đưa bước giải CAPTCHA vào pipeline QA của bạn — lấy API key CaptchaAI và bắt đầu.