Test suite CI Anda merah setiap kali skenario login atau checkout menyentuh halaman yang dilindungi reCAPTCHA v2? Daripada menonaktifkan CAPTCHA di staging — yang membuat pengujian tidak lagi mewakili kondisi produksi — pipeline di panduan ini menyelesaikan CAPTCHA itu secara programatis di setiap run, lewat satu helper CaptchaAI yang dipanggil dari fixture pytest dan Selenium.
Susunan ini teruji untuk tim QA dan automation engineer yang menjalankan CI di GitHub Actions, termasuk yang mengerjakan proyek klien luar negeri lewat kontrak lepas: helper tunggal untuk kirim dan ambil token, fixture pytest yang scope-nya jelas, serta workflow GitHub Actions yang hanya menyalakan CAPTCHA pada test yang memang membutuhkannya — supaya biaya thread CaptchaAI tetap kecil.
Kenapa CAPTCHA Sering Bikin CI Macet
Tim yang menonaktifkan CAPTCHA di staging biasanya berdalih "supaya test lebih cepat", tapi konsekuensinya besar: bug yang hanya muncul saat CAPTCHA aktif — race condition pada callback, token yang kedaluwarsa sebelum submit, id elemen yang berubah — lolos ke production. Pendekatan yang lebih aman adalah membiarkan CAPTCHA tetap aktif di staging dan menyelesaikannya lewat API, sehingga test benar-benar menjalankan jalur yang sama dengan pengguna asli.
Karena CaptchaAI menagih per thread bersamaan, bukan per solve, menjalankan CAPTCHA di setiap test tidak membuat tagihan meledak. Paket BASIC ($15/bulan, 5 thread) sudah cukup untuk kebanyakan pipeline CI skala tim kecil; naikkan ke STANDARD ($30/bulan, 15 thread) hanya kalau job E2E Anda berjalan banyak secara paralel.
Struktur Folder Test Suite
Empat bagian inti — helper CAPTCHA, fixture bersama, dua modul test, dan konfigurasi pytest — sudah cukup untuk pipeline ini:
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
Sebelum menjalankan pipeline ini, pastikan tiga hal berikut sudah beres:
- API key CaptchaAI sudah tersedia sebagai environment variable
CAPTCHAAI_API_KEYdi runner CI. - Chrome dan ChromeDriver versi yang saling cocok sudah terpasang di image CI yang dipakai.
- Environment staging tetap menampilkan CAPTCHA — jangan dinonaktifkan hanya demi mempercepat test, karena itu menghilangkan jalur yang justru paling penting untuk diuji.
Helper CaptchaAI: Kirim dan Ambil Token
Helper ini melakukan tiga hal: kirim sitekey dan URL halaman ke in.php, polling res.php sampai token siap, lalu menyuntikkan token itu ke elemen g-recaptcha-response di browser Selenium. Jeda 15 detik sebelum polling pertama dan interval 5 detik sesudahnya dipilih supaya loop tidak membombardir endpoint sebelum solve biasanya rampung — sesuaikan kalau throughput test suite Anda berbeda.
Skrip juga mencoba memanggil callback reCAPTCHA secara eksplisit lewat ___grecaptcha_cfg, untuk situs yang mengandalkan event callback dan bukan sekadar nilai textarea, supaya tombol submit yang di-enable oleh JavaScript sisi klien ikut terpicu.
# 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)
Fixture Pytest: Browser dan Solver
captcha_solver di-scope session supaya satu instance helper dipakai ulang di seluruh test run, bukan dibuat ulang di tiap test. browser di-scope function dan selalu memanggil quit() di akhir supaya proses Chrome tidak menumpuk di runner CI. Flag --headless, --no-sandbox, dan --disable-dev-shm-usage wajib ada karena runner GitHub Actions default berjalan tanpa display dan dengan memori /dev/shm yang terbatas.
# 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"
Test Login: Menyelesaikan CAPTCHA Sebelum Submit
Dua skenario di sini menutup jalur yang sering luput dari test manual: login yang benar dengan CAPTCHA valid, dan login dengan kredensial salah tapi CAPTCHA tetap terselesaikan — memastikan yang muncul adalah pesan error kredensial, bukan error CAPTCHA yang menutupi bug sesungguhnya.
# 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
Test Checkout: Alur Belanja Lengkap dengan CAPTCHA
Test ini menjalankan alur penuh — tambah item ke keranjang, isi alamat pengiriman, selesaikan CAPTCHA di halaman checkout, lalu tunggu konfirmasi order muncul lewat WebDriverWait. Ini skenario yang paling sering luput kalau tim hanya menguji login: CAPTCHA di checkout biasanya punya sitekey berbeda dari CAPTCHA di login, jadi helper harus dipanggil ulang dengan sitekey halaman yang sedang aktif.
# 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
Konfigurasi pytest.ini
Marker captcha memisahkan test yang menyelesaikan CAPTCHA sungguhan dari test biasa, supaya Anda bisa memilih kapan menjalankannya — penting karena tiap solve punya biaya, sekecil apa pun.
Jalankan
pytest -m captchakhusus saat memang perlu menguji jalur CAPTCHA — di CI terjadwal atau saat review manual, bukan di setiap commit lokal — supaya thread CaptchaAI tidak terpakai sia-sia.
# tests/pytest.ini
[pytest]
markers =
captcha: tests requiring CAPTCHA solving (cost per run)
addopts = -v --tb=short
Workflow GitHub Actions untuk E2E Test
Contoh ini sengaja dijadwalkan mingguan (cron: "0 6 * * 1", tiap Senin) alih-alih di setiap push — cukup untuk menangkap regresi tanpa membebani thread CaptchaAI di setiap commit kecil. Kalau tim Anda butuh sinyal lebih cepat, pindahkan trigger ke pull_request pada branch tertentu saja, bukan ke semua push.
# .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
Pertanyaan Umum
Berapa biaya menjalankan test suite yang menyelesaikan CAPTCHA di CI?
Tiap penyelesaian bernilai beberapa sen, jadi 10 test yang jalan tiap hari biasanya masih di bawah $10/bulan. Karena CaptchaAI menagih per thread bersamaan, bukan per solve, paket BASIC ($15/bulan, 5 thread) sudah cukup untuk kebanyakan pipeline CI skala kecil-menengah. Pakai marker pytest supaya CAPTCHA hanya jalan di test yang memang membutuhkannya.
Bagaimana cara mengecualikan test CAPTCHA saat coding lokal supaya lebih cepat?
Jalankan pytest -m "not captcha" untuk melewati semua test yang ditandai dekorator @pytest.mark.captcha. Anda tetap mendapat feedback cepat dari test lain, dan menyisakan test CAPTCHA untuk run CI atau saat memang sengaja menguji jalur itu.
Apakah pipeline ini bisa dipakai untuk Cloudflare Turnstile atau GeeTest v3, bukan cuma reCAPTCHA v2?
Bisa — polanya sama: kirim ke in.php dengan method yang sesuai tipe CAPTCHA, lalu polling res.php seperti helper di atas. Yang berubah hanya parameter method dan cara menyuntikkan token ke elemen halaman. reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile, dan GeeTest v3 semuanya didukung penuh; hCaptcha dan FunCaptcha belum didukung CaptchaAI saat ini.
Kenapa contoh workflow GitHub Actions ini dijadwalkan tiap Senin, bukan di setiap push?
Supaya thread CAPTCHA tidak terpakai untuk setiap commit kecil. Jadwal mingguan cukup untuk menangkap regresi pada alur login dan checkout; kalau perubahan Anda sering menyentuh area itu, tambahkan trigger pull_request khusus untuk branch yang relevan saja.
Bagaimana kalau runner CI saya jauh dari Indonesia dan polling CAPTCHA sering timeout?
Naikkan nilai timeout polling di helper — 180 detik adalah titik awal yang aman. Kalau tim Anda sudah punya infrastruktur di ap-southeast-1 atau ap-southeast-3, self-hosted runner di region itu biasanya memangkas latensi jaringan ke API CaptchaAI dibanding runner default GitHub yang bisa saja berada jauh di benua lain.
Error yang Sering Muncul dan Solusinya
Sebagian besar kegagalan pipeline ini bukan soal CaptchaAI, melainkan lingkungan CI yang berbeda dari laptop lokal Anda:
- Penyuntikan token gagal — textarea tidak ditemukan. Solusi: periksa ID elemen atau gunakan
querySelector('[name="g-recaptcha-response"]'). - Test lulus di lokal, gagal di CI — versi Chrome berbeda. Solusi: pin versi Chrome di setup CI.
- CAPTCHA tidak muncul di staging — staging menonaktifkan CAPTCHA. Solusi: aktifkan CAPTCHA di konfigurasi environment staging.
- Timeout menunggu hasil solve — jaringan lambat di runner CI. Solusi: naikkan timeout polling ke 180 detik.
- Polling terasa lambat karena runner jauh dari Asia — runner GitHub Actions default ada di region AS/EU. Solusi: pertimbangkan self-hosted runner dekat
ap-southeast-1(Singapura) atauap-southeast-3(Jakarta), atau cukup naikkan timeout.
Panduan Terkait
Untuk memperdalam setup CI dan pola pengujian yang authorized, baca:
Jangan biarkan CAPTCHA memblokir pipeline CI Anda — mulai dengan CaptchaAI.