Test suite registrasi yang berhenti gara-gara CAPTCHA bukan berarti automation Anda salah desain — form pendaftaran memang sengaja dipasangi reCAPTCHA atau Turnstile untuk menahan bot, dan Selenium polos akan selalu berhenti di titik itu. Solusinya bukan mematikan CAPTCHA di staging (itu justru menghilangkan hal yang mau Anda validasi), melainkan menyisipkan solver ke alur test seperti komponen lain: generate data, isi form, selesaikan CAPTCHA, verifikasi hasil. Artikel ini membedah RegistrationTester berbasis Python dan Selenium yang menjalankan skenario registrasi valid, email duplikat, dan password lemah — lengkap dengan integrasi pytest untuk CI.
Kenapa CAPTCHA Jadi Titik Gagal Utama saat Testing Alur Registrasi
Hampir semua form pendaftaran publik memasang CAPTCHA — reCAPTCHA v2 di form yang lebih lama, Turnstile di stack yang lebih baru — supaya bot tidak bisa membuat akun massal. Bagi tim QA, itu berarti empat pekerjaan yang harus jalan otomatis dan berulang setiap ada deployment:
- Memastikan alur pendaftaran memang berfungsi di staging maupun production
- Menjalankan skenario tepi: email duplikat, password lemah, kolom wajib yang kosong
- Menjalankan regression test setiap kali ada deployment baru, bukan cuma sesekali
- Memvalidasi bahwa penempatan widget CAPTCHA sendiri tidak merusak layout atau proses submit
Tim automation dan QA freelance di Indonesia — yang banyak beroperasi lewat kontrak Upwork atau Fastwork untuk klien e-commerce dan startup — biasanya menjalankan regression suite semacam ini di pipeline CI setiap malam. Karena run paralel berarti beberapa CAPTCHA diselesaikan bersamaan, kapasitas thread ikut menentukan seberapa cepat seluruh suite selesai: paket BASIC ($15/bulan, 5 thread) biasanya cukup untuk regression harian tim kecil, sementara STANDARD ($30/bulan, 15 thread) lebih pas kalau CI menjalankan beberapa instance browser sekaligus.
Masalah yang Perlu Diantisipasi Sebelum Mulai
Sebelum masuk ke kode, kenali dulu empat jebakan yang paling sering bikin RegistrationTester salah laporan saat pertama kali dipasang di form baru:
| Masalah | Penyebab | Solusi |
|---|---|---|
| Test melaporkan gagal padahal registrasi sebenarnya berhasil | Indikator sukses di _check_success() tidak cocok dengan teks situs Anda |
Tambahkan teks sukses spesifik situs Anda ke daftar success_indicators |
| CAPTCHA tidak terdeteksi oleh selector | Widget CAPTCHA baru muncul setelah delay atau render JS | Tambahkan time.sleep(2) sebelum memanggil _solve_captcha() |
| Elemen form tidak ditemukan | Struktur HTML form berbeda dari asumsi kode | Sesuaikan CSS selector di _fill() dengan markup situs Anda |
| Token CAPTCHA sudah kedaluwarsa saat submit | Token diselesaikan terlalu awal, sebelum field lain terisi | Pindahkan _solve_captcha() agar dipanggil tepat sebelum tombol submit diklik |
Dengan daftar ini di tangan, kode di bawah jadi lebih mudah dibaca — Anda sudah tahu bagian mana yang paling sering perlu disesuaikan dengan situs Anda sendiri.
Alur Kerja Pengujian Registrasi
Skema di bawah menggambarkan urutan yang dipakai RegistrationTester: buat data uji sintetis, isi form sekaligus skenario tepi, selesaikan CAPTCHA yang muncul, lalu verifikasi akun benar-benar terbuat.
┌──────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────┐
│ Test Data │────▶│ Fill Form + │────▶│ Solve │────▶│ Verify │
│ Generator │ │ Edge Cases │ │ CAPTCHA │ │ Account │
└──────────┘ └───────────────┘ └────────────┘ └────────────┘
Implementasi Test Registrasi dengan Python dan Selenium
Bangun ini dalam dua lapis: generator data uji yang selalu unik, dan tester yang menjalankan Selenium sekaligus memanggil CaptchaAI setiap kali widget CAPTCHA terdeteksi di halaman.
Generator Data Uji
TestUser membuat email, username, dan password baru setiap kali dipanggil, dengan timestamp dan string acak supaya tidak pernah bentrok antar run.
Karena UU Pelindungan Data Pribadi (UU 27/2022) membatasi pemrosesan data pribadi asli, data ini sengaja sintetis — domain
@testmail.example.combukan alamat sungguhan. Jangan pernah mengganti generator ini dengan data pengguna produksi asli hanya demi kenyamanan testing.
import random
import string
import time
class TestUser:
def __init__(self, prefix="test"):
ts = int(time.time())
rand = ''.join(random.choices(string.ascii_lowercase, k=4))
self.first_name = f"{prefix}_{rand}"
self.last_name = "User"
self.email = f"{prefix}_{ts}_{rand}@testmail.example.com"
self.username = f"{prefix}_{ts}_{rand}"
self.password = f"Test!{ts}{rand.upper()}"
def as_dict(self):
return {
"first_name": self.first_name,
"last_name": self.last_name,
"email": self.email,
"username": self.username,
"password": self.password,
}
Kelas Penguji Registrasi
CaptchaSolver membungkus dua endpoint CaptchaAI: in.php untuk mengirim task, res.php untuk polling hasil. RegistrationTester memakainya di _solve_captcha() — mendeteksi dulu apakah widget yang tampil Turnstile (.cf-turnstile) atau reCAPTCHA ([data-sitekey]), baru memanggil metode solve yang sesuai dan menyuntikkan token ke field tersembunyi sebelum submit. Tiga skenario diuji: registrasi valid, email duplikat, dan password lemah — masing-masing mencatat hasil PASS/FAIL beserta pesan error yang tertangkap di halaman.
import time
import requests
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
class CaptchaSolver:
BASE = "https://ocr.captchaai.com"
def __init__(self, api_key):
self.api_key = api_key
def solve_recaptcha(self, sitekey, pageurl):
return self._solve({
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": pageurl,
})
def solve_turnstile(self, sitekey, pageurl):
return self._solve({
"method": "turnstile",
"sitekey": sitekey,
"pageurl": pageurl,
})
def _solve(self, params, initial_wait=10):
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(f"{self.BASE}/in.php", data=params).json()
if resp["status"] != 1:
raise Exception(resp["request"])
task_id = resp["request"]
time.sleep(initial_wait)
for _ in range(60):
result = requests.get(
f"{self.BASE}/res.php",
params={"key": self.api_key, "action": "get", "id": task_id, "json": 1},
).json()
if result["request"] == "CAPCHA_NOT_READY":
time.sleep(5)
continue
if result["status"] == 1:
return result["request"]
raise Exception(result["request"])
raise TimeoutError("Timed out")
class RegistrationTester:
def __init__(self, api_key, base_url):
self.solver = CaptchaSolver(api_key)
self.base_url = base_url
self.driver = webdriver.Chrome()
self.wait = WebDriverWait(self.driver, 10)
self.results = []
def _fill(self, selector, value):
el = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
el.clear()
el.send_keys(value)
def _solve_captcha(self):
html = self.driver.page_source
page_url = self.driver.current_url
# Turnstile
turnstile = self.driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
if turnstile:
sitekey = turnstile[0].get_attribute("data-sitekey")
token = self.solver.solve_turnstile(sitekey, page_url)
self.driver.execute_script(
f'document.querySelector("[name=cf-turnstile-response]").value = "{token}";'
)
return
# reCAPTCHA
recaptcha = self.driver.find_elements(By.CSS_SELECTOR, "[data-sitekey]")
if recaptcha and "recaptcha" in html.lower():
sitekey = recaptcha[0].get_attribute("data-sitekey")
token = self.solver.solve_recaptcha(sitekey, page_url)
self.driver.execute_script(
f'document.querySelector("[name=g-recaptcha-response]").value = "{token}";'
)
def _get_errors(self):
"""Collect any visible error messages on the page."""
error_selectors = [
".error", ".alert-danger", ".form-error",
"[role='alert']", ".validation-error",
]
errors = []
for sel in error_selectors:
for el in self.driver.find_elements(By.CSS_SELECTOR, sel):
text = el.text.strip()
if text:
errors.append(text)
return errors
def _check_success(self):
"""Check if registration succeeded."""
html = self.driver.page_source.lower()
url = self.driver.current_url.lower()
success_indicators = [
"welcome", "account created", "verify your email",
"registration successful", "thank you for registering",
]
return any(ind in html or ind in url for ind in success_indicators)
# --- Test Cases ---
def test_valid_registration(self):
"""Test: Valid registration should succeed."""
user = TestUser()
self.driver.get(f"{self.base_url}/register")
self._fill("[name='firstName'], #first-name", user.first_name)
self._fill("[name='lastName'], #last-name", user.last_name)
self._fill("[name='email'], #email", user.email)
self._fill("[name='username'], #username", user.username)
self._fill("[name='password'], #password", user.password)
confirm_fields = self.driver.find_elements(By.CSS_SELECTOR, "[name='confirmPassword'], #confirm-password")
if confirm_fields:
confirm_fields[0].clear()
confirm_fields[0].send_keys(user.password)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
success = self._check_success()
self.results.append({
"test": "valid_registration",
"passed": success,
"user": user.email,
"errors": self._get_errors() if not success else [],
})
return success
def test_duplicate_email(self):
"""Test: Duplicate email should show error."""
user = TestUser()
# First registration
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", user.password)
self._fill("[name='firstName'], #first-name", user.first_name)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
# Second registration with same email
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", user.password)
self._fill("[name='firstName'], #first-name", "Duplicate")
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
errors = self._get_errors()
has_error = len(errors) > 0 or not self._check_success()
self.results.append({
"test": "duplicate_email",
"passed": has_error,
"errors": errors,
})
return has_error
def test_weak_password(self):
"""Test: Weak password should be rejected."""
user = TestUser()
self.driver.get(f"{self.base_url}/register")
self._fill("[name='email'], #email", user.email)
self._fill("[name='password'], #password", "123") # Weak password
self._fill("[name='firstName'], #first-name", user.first_name)
self._solve_captcha()
self.driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
time.sleep(3)
errors = self._get_errors()
rejected = len(errors) > 0 or not self._check_success()
self.results.append({
"test": "weak_password",
"passed": rejected,
"errors": errors,
})
return rejected
def run_all(self):
"""Run all registration tests."""
tests = [
self.test_valid_registration,
self.test_duplicate_email,
self.test_weak_password,
]
for test_fn in tests:
try:
test_fn()
except Exception as e:
self.results.append({
"test": test_fn.__name__,
"passed": False,
"errors": [str(e)],
})
return self.results
def report(self):
passed = sum(1 for r in self.results if r["passed"])
total = len(self.results)
lines = [f"Registration Tests: {passed}/{total} passed", "-" * 40]
for r in self.results:
status = "PASS" if r["passed"] else "FAIL"
lines.append(f" [{status}] {r['test']}")
if r.get("errors"):
for err in r["errors"]:
lines.append(f" {err}")
return "\n".join(lines)
def close(self):
self.driver.quit()
Menjalankan Test Suite Registrasi
Instansiasi RegistrationTester dengan API key dan base URL staging, jalankan run_all(), lalu cetak laporannya:
tester = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
try:
tester.run_all()
print(tester.report())
finally:
tester.close()
Keluaran:
Registration Tests: 3/3 passed
----------------------------------------
[PASS] valid_registration
[PASS] duplicate_email
[PASS] weak_password
Integrasi dengan pytest
Fixture module-scope di bawah membuka satu instance browser untuk seluruh file test, supaya CaptchaAI tidak perlu menyelesaikan CAPTCHA baru di setiap assertion — cukup satu kali per skenario:
import pytest
@pytest.fixture(scope="module")
def tester():
t = RegistrationTester("YOUR_API_KEY", "https://staging.example.com")
yield t
t.close()
def test_valid_registration(tester):
assert tester.test_valid_registration(), "Valid registration should succeed"
def test_duplicate_email_rejected(tester):
assert tester.test_duplicate_email(), "Duplicate email should be rejected"
def test_weak_password_rejected(tester):
assert tester.test_weak_password(), "Weak password should be rejected"
Pertanyaan Umum
Berapa lama tambahan waktu yang dibutuhkan CAPTCHA dalam satu siklus test registrasi?
reCAPTCHA v2 biasanya selesai di bawah 60 detik, sementara Turnstile di bawah 10 detik. Kalau batas waktu (timeout) di test runner Anda terlalu ketat, skenario dengan reCAPTCHA v2 bisa dianggap gagal padahal solver masih polling — longgarkan batas waktu test suite, bukan cuma WebDriverWait di Selenium.
Apakah RegistrationTester ini otomatis menangani reCAPTCHA v3 atau GeeTest v3?
Belum secara otomatis. _solve_captcha() di atas hanya mendeteksi Turnstile dan reCAPTCHA v2 lewat selector DOM. CaptchaAI sendiri mendukung reCAPTCHA v3, reCAPTCHA Enterprise, dan GeeTest v3 — untuk form yang memakainya, tambahkan cabang deteksi baru yang memanggil method API userrecaptcha (berlaku untuk semua varian reCAPTCHA) atau geetest.
Berapa banyak thread yang dibutuhkan untuk menjalankan regression suite ini secara paralel?
Tergantung berapa browser yang berjalan bersamaan di CI. Satu thread menangani satu CAPTCHA yang sedang diselesaikan; kalau pipeline Anda menjalankan 5 instance Chrome paralel, paket BASIC ($15/bulan, 5 thread) sudah cukup, dan Anda bisa naik ke STANDARD ($30/bulan, 15 thread) begitu matrix testing bertambah besar.
Bisakah pengujian ini dijalankan di CI/CD tanpa GUI?
Ya. Tambahkan options.add_argument("--headless") ke Chrome options, dan simpan API key CaptchaAI sebagai secret atau environment variable di pipeline CI — jangan hardcode langsung di script.
Apakah data uji sintetis di atas aman dari sisi UU PDP?
Ya, selama tetap sintetis. TestUser menghasilkan email di domain @testmail.example.com dan password acak setiap run, jadi tidak ada data pribadi asli yang diproses — aman dari perspektif UU Pelindungan Data Pribadi (UU 27/2022). Jangan pernah mengganti generator ini dengan data pengguna produksi asli hanya demi kenyamanan.
Ringkasan Cepat
- Deteksi widget dulu (Turnstile lewat
.cf-turnstile, reCAPTCHA lewat[data-sitekey]), baru panggil solver yang sesuai - Selesaikan CAPTCHA tepat sebelum submit supaya token tidak keburu kedaluwarsa
- Pakai data uji sintetis (
@testmail.example.com), jangan pernah data pengguna produksi asli - Sesuaikan kapasitas thread (BASIC, STANDARD, atau di atasnya) dengan jumlah browser paralel di pipeline CI Anda
Panduan Terkait
Jalankan regression test registrasi tanpa terhambat CAPTCHA — gunakan CaptchaAI.