عندما يصل خط اختباراتك الآلي إلى صفحة تسجيل دخول أو إتمام شراء محمية بـ reCAPTCHA v2، تتوقف المجموعة بالكامل: لا يستطيع Selenium النقر على مربع «لست روبوتاً»، فتفشل كل الاختبارات اللاحقة دون أي خلل حقيقي في التطبيق. الحل ليس تعطيل التحقق في بيئة الاختبار، بل تمرير التحدي إلى خدمة تحلّه برمجياً وتعيد رمزاً صالحاً تحقنه في الصفحة.
في هذا الدليل نبني خط أنابيب اختبار آلي كامل يعتمد على pytest وSelenium وGitHub Actions، ويستدعي CaptchaAI لحل reCAPTCHA v2 أثناء اختبارات النهاية-إلى-النهاية (E2E). النتيجة مجموعة اختبارات تعمل داخل CI بلا تدخل بشري، وتغطي مسارات حقيقية مثل تسجيل الدخول وإتمام الشراء تماماً كما يمر بها المستخدم.
لماذا يحتاج خط أنابيب الاختبار الآلي إلى حل CAPTCHA؟
معظم الفرق تعالج هذه العقبة بتعطيل CAPTCHA في بيئة staging، لكن ذلك يخلق فجوة خطيرة: المسار الذي تختبره لم يعد مطابقاً لما يراه المستخدم في الإنتاج، وقد ينكسر تكامل الاستمارة أو رد النداء (callback) دون أن يلاحظه أحد حتى لحظة الإطلاق.
تخيّل فريق QA في متجر إلكتروني بالقاهرة يشغّل مجموعة اختبارات ليلية على بيئة staging تحاكي الإنتاج بالكامل. مع إبقاء reCAPTCHA v2 مفعّلاً وحلّه عبر CaptchaAI، يبقى اختبار «إتمام الشراء» أميناً على السلوك الحقيقي عبر الخطوات التالية:
- ملء الاستمارة ببيانات الشحن كما يفعل المستخدم.
- حل تحدي reCAPTCHA v2 برمجياً بدل تعطيله.
- إرسال الطلب والتأكد من ظهور صفحة التأكيد.
كل ذلك يجري دون أن يجلس مهندس أمام الشاشة. هذا التوازن بين الأتمتة الآمنة وتطابق البيئة هو ما يجعل حل CAPTCHA داخل الاختبار خياراً أفضل من إخفائه.
هيكل مشروع الاختبار
نبدأ بتنظيم الملفات بحيث نفصل منطق حل CAPTCHA عن منطق المتصفح عن حالات الاختبار نفسها:
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
يعزل هذا التقسيم مساعد CaptchaAI داخل helpers/captcha.py، ويترك ملفات test_*.py نظيفة تركّز على منطق العمل. ملف conftest.py يوفّر التركيبات (fixtures) المشتركة، بينما يضبط pytest.ini الأعلام (markers) وخيارات التشغيل.
مساعد حل CAPTCHA في CaptchaAI
جوهر خط الأنابيب صنف واحد يغلّف دورة الحل الكاملة: أرسل تحدي reCAPTCHA إلى CaptchaAI، استطلع النتيجة دورياً، ثم احقن الرمز في المتصفح.
# 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)
يقرأ الصنف مفتاح الـ API من متغير البيئة CAPTCHAAI_API_KEY بدل تضمينه في الشيفرة؛ قاعدة أساسية لتأمين بيانات الاعتماد داخل CI. أما دورة الحل فتمرّ بثلاث مراحل واضحة:
- الإرسال: تُرسل الدالة
solve_recaptchaمفتاح الموقع (sitekey) وعنوان الصفحة إلى نقطة النهايةin.phpبالطريقةuserrecaptcha. - الاستطلاع: تستفسر عن
res.phpكل خمس ثوانٍ حتى يعود الرمز أو تنتهي المهلة. - الحقن: تحقن
inject_tokenالرمز في حقلg-recaptcha-responseوتشغّل رد النداء (callback) الخاص بالودجت إن وُجد.
هذه المرحلة الأخيرة مهمة، لأن مجرد كتابة الرمز في الحقل لا يكفي أحياناً لتفعيل الاستمارة ما لم يُستدعَ رد النداء المرتبط بالودجت.
تركيبات pytest المشتركة
تحوّل التركيبات (fixtures) إعداد المتصفح وحل CAPTCHA إلى موارد قابلة لإعادة الاستخدام عبر كل الاختبارات:
# 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"
تُنشأ تركيبة captcha_solver مرة واحدة لكل جلسة اختبار لتوفير النفقات، بينما تُنشأ تركيبة browser لكل دالة اختبار كي تبدأ كل حالة بمتصفح نظيف. لاحظ تشغيل Chrome في وضع headless مع الأعلام المناسبة للعمل داخل حاويات CI.
اختبار تسجيل الدخول مع reCAPTCHA v2
أول حالة عملية مسار تسجيل دخول كامل: يملأ الاستمارة، يحل reCAPTCHA v2، ثم يتحقق من إعادة التوجيه إلى لوحة التحكم.
# 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("test@example.com")
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("wrong@example.com")
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
يستخرج الاختبار مفتاح الموقع من عنصر g-recaptcha ديناميكياً، فلا حاجة لتضمينه يدوياً. أما الحالة الثانية فتتحقق من أن بيانات الاعتماد الخاطئة تُظهر رسالة خطأ حتى مع حل CAPTCHA صحيح؛ تمييز مهم يضمن أنك تختبر منطق المصادقة لا آلية التحقق.
اختبار إتمام الشراء
مسار إتمام الشراء أطول: إضافة منتج إلى السلة، ملء بيانات الشحن، حل CAPTCHA، ثم انتظار صفحة التأكيد.
# 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
يستخدم هذا الاختبار WebDriverWait لانتظار ظهور تأكيد الطلب بدل مهلة ثابتة، ما يجعله أكثر استقراراً على الشبكات البطيئة داخل CI ويقلّل حالات الفشل العشوائية (flaky tests).
ضبط pytest
نعرّف علامة (marker) مخصّصة تفصل الاختبارات التي تستهلك عمليات حل CAPTCHA عن بقية المجموعة:
# tests/pytest.ini
[pytest]
markers =
captcha: tests requiring CAPTCHA solving (cost per run)
addopts = -v --tb=short
بفضل هذه العلامة يمكنك تشغيل اختبارات CAPTCHA وحدها عند الحاجة، أو استبعادها في عمليات الدمج السريعة كي تبقى دورة التطوير خفيفة.
تشغيل خط أنابيب الاختبار الآلي عبر GitHub Actions
أخيراً نربط كل شيء في سير عمل يعمل عند كل دفع إلى main وأسبوعياً كل اثنين صباحاً:
# .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
يعتمد سير العمل على مبدأين أساسيين:
- تمرير مفتاح الـ API عبر أسرار المستودع (secrets) لا كنص ظاهر داخل الملف.
- تثبيت إصدار Chrome لضمان تطابق البيئة بين جهازك وخوادم CI وتجنّب فشل يظهر في CI وحده.
حل المشكلات الشائعة
| المشكلة | السبب المحتمل | الحل |
|---|---|---|
| فشل حقن الرمز | تعذّر العثور على حقل الإدخال | تحقق من معرّف العنصر أو استخدم querySelector('[name="g-recaptcha-response"]') |
| ينجح الاختبار محلياً ويفشل في CI | اختلاف إصدار Chrome | ثبّت إصدار Chrome في إعداد CI |
| لا يظهر CAPTCHA في بيئة staging | staging يعطّل التحقق | فعّل CAPTCHA في إعدادات بيئة staging |
| انتهاء المهلة أثناء انتظار الحل | بطء الشبكة في CI | ارفع مهلة الاستطلاع إلى 180 ثانية |
الأسئلة الشائعة
كم تبلغ تكلفة تشغيل اختبارات CAPTCHA عبر CaptchaAI؟
يعتمد تسعير CaptchaAI على عدد الخيوط المتزامنة (threads) شهرياً لا على كل عملية حل، مع حلول غير محدودة داخل كل خطة. تبدأ خطة BASIC من 15 دولاراً أمريكياً شهرياً وتشمل 5 خيوط، وهو أكثر من كافٍ لمجموعة اختبارات E2E ليلية. راجع الأسعار المحدّثة على captchaai.com/pricing.
هل يدعم هذا المسار reCAPTCHA v3 أو Cloudflare Turnstile؟
نعم. البنية نفسها (إرسال، استطلاع، حقن) تعمل مع reCAPTCHA v3 وCloudflare Turnstile وGeeTest v3، مع تغيير قيمة method وحقل الرمز المُعاد. يكفي إضافة دالة حل مماثلة لكل نوع مع الحفاظ على حقل الرمز الصحيح لكل ودجت. لاحظ أن CaptchaAI لا يحل حالياً hCaptcha أو FunCaptcha.
هل حلّ CAPTCHA داخل الاختبارات الآلية أمر مسموح؟
نعم، ما دمت تختبر تطبيقك أو نظاماً تملك صلاحية اختباره. هذا هو الاستخدام المقصود: اختبار جودة (QA) وأتمتة موثوقة على بيئاتك الخاصة، لا التعامل مع مواقع لا تملكها.
ما الفرق بين تعطيل CAPTCHA في staging وحلّه فعلياً؟
تعطيل CAPTCHA يجعل بيئة الاختبار غير مطابقة للإنتاج، فقد تنكسر الاستمارة أو رد النداء دون أن يُكتشف. أما حلّه عبر CaptchaAI فيحافظ على تطابق المسار مع تجربة المستخدم الحقيقية ويكشف أعطال التكامل مبكراً.
أدلة ذات صلة
شغّل أول اختبار E2E يحل reCAPTCHA v2 تلقائياً — ابدأ مع CaptchaAI.