Hướng Dẫn Thực Hành

Xây dựng quy trình thử nghiệm tự động với CaptchaAI

Cách nhanh nhất để bộ test E2E hết bị treo ở bước reCAPTCHA v2 là giải CAPTCHA ngay trong fixture, thay vì tắt CAPTCHA trên staging rồi hy vọng production không bao giờ khác staging. Bài này lắp một CaptchaTestHelper gọi API CaptchaAI vào pytest, chạy qua Selenium, và đưa cả pipeline lên GitHub Actions.

Đây cũng là cách nhiều đội QA ở các công ty outsource tại TP.HCM và Hà Nội đang xử lý bài toán này: một team kiểm thử luồng đăng nhập và checkout của một trang e-commerce đang dựng ở môi trường staging, CI chạy nightly, và CAPTCHA thật phải được giải trong test — chứ không phải tắt CAPTCHA trên staging, vì cấu hình đó không còn phản ánh đúng production nữa.

Toàn bộ pipeline chỉ xoay quanh bốn bước lặp lại ở mọi test có CAPTCHA:

  • Lấy sitekeypageurl ngay trên trang đang test
  • Gửi task tới in.php qua CaptchaTestHelper
  • Polling res.php cho đến khi có token
  • Bơm token vào form bằng Selenium rồi submit như test bình thường

Cấu trúc thư mục test

Ba nhóm file tách riêng: helper gọi CaptchaAI, fixture dùng chung, và test theo từng luồng nghiệp vụ.

tests/
├── conftest.py          # Shared fixtures
├── helpers/
│   ├── captcha.py       # CaptchaAI integration
│   └── browser.py       # Selenium helpers
├── test_login.py        # Login flow tests
├── test_checkout.py     # Checkout flow tests
└── pytest.ini           # Config

Class hỗ trợ giải CAPTCHA cho test tự động

CaptchaTestHelper gói gọn toàn bộ luồng gửi task và polling res.php thành hai phương thức: solve_recaptcha() để lấy token, inject_token() để bơm token đó vào form trong Selenium. Toàn bộ fixture và test bên dưới chỉ gọi hai phương thức này.

# tests/helpers/captcha.py
import requests
import time
import os

class CaptchaTestHelper:
    """Solve CAPTCHAs during automated tests."""

    def __init__(self):
        self.api_key = os.environ.get("CAPTCHAAI_API_KEY")
        if not self.api_key:
            raise EnvironmentError("CAPTCHAAI_API_KEY required for CAPTCHA tests")

    def solve_recaptcha(self, sitekey, pageurl):
        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()
        if result.get("status") != 1:
            raise RuntimeError(f"Submit failed: {result.get('request')}")

        task_id = result["request"]
        time.sleep(15)

        for _ in range(24):
            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(data["request"])
            time.sleep(5)

        raise TimeoutError("CAPTCHA solve timeout in test")

    def inject_token(self, driver, token):
        """Inject solved token into Selenium browser."""
        driver.execute_script(
            'document.getElementById("g-recaptcha-response").value = arguments[0];',
            token,
        )
        # Trigger callback if available
        driver.execute_script("""
            if (typeof ___grecaptcha_cfg !== 'undefined') {
                var clients = ___grecaptcha_cfg.clients;
                for (var key in clients) {
                    var client = clients[key];
                    for (var prop in client) {
                        var val = client[prop];
                        if (val && typeof val === 'object') {
                            for (var inner in val) {
                                if (typeof val[inner] === 'function') {
                                    val[inner](arguments[0]);
                                    return;
                                }
                            }
                        }
                    }
                }
            }
        """, token)

Các file test bên dưới không cần biết in.php hay res.php — chỉ gọi hai phương thức công khai của class này.


Fixture dùng chung trong Pytest

Hai fixture dưới đây là tất cả những gì các test cần: captcha_solver khởi tạo helper một lần cho cả session, browser mở Chrome headless cho từng test rồi tự đóng lại. base_url trỏ vào staging để không ai lỡ chạy test nhắm vào production.

# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from helpers.captcha import CaptchaTestHelper

@pytest.fixture(scope="session")
def captcha_solver():
    return CaptchaTestHelper()

@pytest.fixture(scope="function")
def browser():
    options = Options()
    options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    driver = webdriver.Chrome(options=options)
    driver.implicitly_wait(10)
    yield driver
    driver.quit()

@pytest.fixture(scope="session")
def base_url():
    return "https://staging.example.com"

scope="session" giúp CaptchaAI chỉ khởi tạo một lần cho cả lần chạy, thay vì mỗi test một object.


Kiểm tra luồng đăng nhập có CAPTCHA

Hai test dưới đây dùng chung một khuôn: lấy sitekey từ DOM, giải qua captcha_solver, bơm token, rồi mới submit form. Test thứ hai chứng minh CAPTCHA hợp lệ không che giấu lỗi sai mật khẩu — hai điều kiện lỗi vẫn phải được kiểm tra độc lập.

# tests/test_login.py
import pytest
from selenium.webdriver.common.by import By

class TestLogin:
    def test_valid_login_with_captcha(self, browser, captcha_solver, base_url):
        """Test that login succeeds when CAPTCHA is solved correctly."""
        browser.get(f"{base_url}/login")

        # Fill form
        browser.find_element(By.ID, "email").send_keys("[email protected]")
        browser.find_element(By.ID, "password").send_keys("testpassword123")

        # Solve CAPTCHA
        sitekey = browser.find_element(
            By.CLASS_NAME, "g-recaptcha"
        ).get_attribute("data-sitekey")

        token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
        captcha_solver.inject_token(browser, token)

        # Submit
        browser.find_element(By.ID, "login-btn").click()

        # Assert redirect to dashboard
        assert "/dashboard" in browser.current_url
        assert browser.find_element(By.CLASS_NAME, "welcome-message")

    def test_invalid_credentials_with_captcha(self, browser, captcha_solver, base_url):
        """Test that wrong credentials show error even with valid CAPTCHA."""
        browser.get(f"{base_url}/login")

        browser.find_element(By.ID, "email").send_keys("[email protected]")
        browser.find_element(By.ID, "password").send_keys("wrongpass")

        sitekey = browser.find_element(
            By.CLASS_NAME, "g-recaptcha"
        ).get_attribute("data-sitekey")

        token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
        captcha_solver.inject_token(browser, token)

        browser.find_element(By.ID, "login-btn").click()

        error = browser.find_element(By.CLASS_NAME, "error-message")
        assert "Invalid" in error.text

Bỏ test thứ hai là để lọt một bug âm thầm: form chấp nhận mọi mật khẩu miễn CAPTCHA hợp lệ.


Kiểm tra luồng thanh toán end-to-end

Test này đi hết một hành trình mua hàng thật: thêm sản phẩm vào giỏ, điền địa chỉ giao hàng, giải CAPTCHA ở trang checkout, rồi chờ trang xác nhận đơn hàng xuất hiện. Đây thường là test chậm nhất trong bộ vì phải qua nhiều trang, nên đặt timeout đủ rộng cho WebDriverWait.

# tests/test_checkout.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class TestCheckout:
    def test_checkout_flow_with_captcha(self, browser, captcha_solver, base_url):
        """Full checkout flow: add item, fill form, solve CAPTCHA, confirm."""
        # Add item to cart
        browser.get(f"{base_url}/products/test-item")
        browser.find_element(By.ID, "add-to-cart").click()

        # Go to checkout
        browser.get(f"{base_url}/checkout")

        # Fill shipping
        browser.find_element(By.ID, "address").send_keys("123 Test St")
        browser.find_element(By.ID, "city").send_keys("Test City")
        browser.find_element(By.ID, "zip").send_keys("12345")

        # Solve CAPTCHA on checkout page
        captcha_el = browser.find_element(By.CLASS_NAME, "g-recaptcha")
        sitekey = captcha_el.get_attribute("data-sitekey")

        token = captcha_solver.solve_recaptcha(sitekey, browser.current_url)
        captcha_solver.inject_token(browser, token)

        # Submit order
        browser.find_element(By.ID, "place-order").click()

        # Wait for confirmation
        wait = WebDriverWait(browser, 15)
        confirmation = wait.until(
            EC.presence_of_element_located((By.CLASS_NAME, "order-confirmation"))
        )
        assert "Thank you" in confirmation.text

Đặt riêng ở test_checkout.py để chạy lại một mình khi debug, khỏi kéo theo cả nhóm test đăng nhập.


Cấu hình pytest.ini cho test có CAPTCHA

Marker captcha cho phép tách riêng các test tốn phí giải CAPTCHA khỏi phần còn lại của bộ test — dùng -m captcha khi cần chạy đầy đủ, -m "not captcha" khi chỉ muốn chạy nhanh.

# tests/pytest.ini
[pytest]
markers =
    captcha: tests requiring CAPTCHA solving (cost per run)
addopts = -v --tb=short

CI chạy -m captcha cho bộ đầy đủ; dev chạy -m "not captcha" cục bộ để không tốn thread mỗi lần lưu file.


Tích hợp pipeline test tự động vào GitHub Actions

Workflow bên dưới chạy bộ E2E mỗi lần push lên main và thêm một lần chạy định kỳ vào sáng thứ Hai — đủ để bắt các lỗi hạ tầng phát sinh ngoài giờ làm việc mà không tốn CI minutes chạy mỗi commit.

# .github/workflows/e2e-tests.yml
name: E2E Tests

on:
  push:
    branches: [main]
  schedule:

    - cron: "0 6 * * 1"  # Weekly Monday 6 AM

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:

      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install pytest selenium requests

      - name: Install Chrome
        uses: browser-actions/setup-chrome@latest

      - name: Run E2E tests
        env:
          CAPTCHAAI_API_KEY: ${{ secrets.CAPTCHAAI_API_KEY }}
        run: pytest tests/ -m captcha -v

Ghim version Chrome cụ thể nếu cần khớp máy local thay vì dùng latest.

Khai báo CAPTCHAAI_API_KEY trong GitHub Secrets trước lần chạy đầu, nếu không job fail ngay ở bước gọi CaptchaAI.


Các lỗi thường gặp và cách xử lý

Bốn lỗi dưới đây chiếm phần lớn ticket "test CAPTCHA bị fail" mà các đội QA gặp phải khi mới lắp pipeline này — kèm một lưu ý riêng cho runner CI đặt xa datacenter của CaptchaAI.

Vấn đề Nguyên nhân Cách xử lý
Bơm token thất bại Không tìm thấy textarea g-recaptcha-response Kiểm tra lại ID phần tử hoặc dùng querySelector('[name="g-recaptcha-response"]')
Test chạy được ở máy local, fail trên CI Phiên bản Chrome trên runner khác với máy local Ghim phiên bản Chrome cụ thể trong bước cài đặt CI
CAPTCHA không xuất hiện trên staging Môi trường staging đang tắt CAPTCHA Bật CAPTCHA trong config env của staging trước khi chạy bộ test này
Hết thời gian chờ khi giải Mạng chậm hoặc runner CI đặt xa Tăng timeout polling lên 180 giây; nếu runner đặt ở khu vực xa CaptchaAI, cân nhắc tăng thêm

Dòng cuối chỉ xuất hiện khi runner CI đặt xa datacenter CaptchaAI — tăng timeout trước, đừng vội nghi ngờ token sai.


Câu hỏi thường gặp

Team QA hay hỏi những điều sau khi mới lắp pipeline này.

Nên chọn plan CaptchaAI nào cho pipeline CI?

BASIC ($15/tháng, 5 thread) đủ cho vài chục test CAPTCHA chạy tuần tự mỗi ngày trong CI. Nếu bộ test chạy song song nhiều luồng hoặc nightly build có hàng trăm case, chuyển sang STANDARD ($30/tháng, 15 thread) hoặc ADVANCE ($90/tháng, 50 thread) để không bị nghẽn ở bước giải.

Chạy bộ test CAPTCHA mỗi ngày tốn bao nhiêu?

Chi phí phụ thuộc vào plan (thread) chứ không tính theo từng lần giải riêng lẻ — mỗi thread cho phép giải không giới hạn trong tháng. Một bộ 10 test chạy mỗi ngày dùng rất ít trong số 5 thread của BASIC, nên gần như không tốn thêm ngoài phí thuê bao cố định.

Có thể mock CaptchaTestHelper trong unit test không?

Có. Mock solve_recaptcha() trong test đơn vị để không tốn thread và chạy nhanh hơn; chỉ gọi CaptchaAI thật trong test E2E — đó là nơi cần xác nhận toàn bộ luồng hoạt động.

Token giải xong nhưng Selenium vẫn báo lỗi thì kiểm tra gì trước?

Kiểm tra ba chỗ theo thứ tự: ID của textarea g-recaptcha-response có đúng không, callback ___grecaptcha_cfg có được kích hoạt sau khi gán token không, và phiên bản Chrome trên CI có khớp với phiên bản đã test ở local không.

Pipeline này có dùng được cho Cloudflare Turnstile hay GeeTest không?

Có, luồng gửi task → polling → dùng token giữ nguyên, chỉ đổi tham số method khi gọi in.php: turnstile cho Cloudflare Turnstile, geetest cho GeeTest v3. GeeTest v4 hiện chưa được CaptchaAI hỗ trợ, vẫn đang ở trạng thái sắp ra mắt.


Hướng dẫn liên quan


Lấy API key CaptchaAI và gắn CaptchaTestHelper vào bộ test đầu tiên trong vài phút — bắt đầu với CaptchaAI.

Os comentários estão desativados para este artigo.