Suite pengujian portal KRS Anda tidak perlu dimatikan hanya karena reCAPTCHA v2 muncul di halaman login: ambil sitekey dari HTML, kirim ke API CaptchaAI, tunggu token, lalu tempelkan token itu ke field g-recaptcha-response pada form yang sedang diuji. Empat langkah itu mengubah skenario yang selama ini dikerjakan manual — satu orang QA duduk mengklik gambar sebelum setiap run — menjadi pengujian end-to-end yang bisa dijalankan di CI. Artikel ini menunjukkan pola tersebut untuk alur khas sistem akademik: login mahasiswa, pencarian mata kuliah, pengambilan KRS, kuota kursi, dan daftar tunggu.
Kenapa masa KRS adalah kasus uji paling berat di kampus
Beban puncak sistem akademik Indonesia sangat terkonsentrasi: jendela pengisian KRS hanya terbuka beberapa hari per semester, dan ribuan mahasiswa masuk ke SIAKAD dalam rentang jam yang sama. Justru di situ perlindungan anti-bot bekerja paling agresif, karena login berulang, pencarian kelas beruntun, dan pengambilan kursi bersamaan adalah pola yang persis ditargetkan reCAPTCHA v2.
Akibatnya, skenario yang paling perlu diuji sebelum KRS dibuka — uji beban ratusan sesi mahasiswa simulasi, perebutan kursi terakhir, perilaku daftar tunggu saat kuota nol — justru yang paling sering terhalang tantangan. Menonaktifkan CAPTCHA di staging memang paling bersih, tetapi tim yang perlu mereproduksi perilaku produksi tetap butuh cara menyelesaikannya secara terprogram.
Satu catatan kepatuhan: pola ini untuk portal milik institusi Anda sendiri atau sistem yang Anda punya izin uji tertulis. Data mahasiswa termasuk data pribadi menurut UU 27/2022 (UU PDP), jadi pakai akun uji dan dataset sintetis.
Titik-titik CAPTCHA yang perlu dipetakan sebelum menulis tes
Petakan dulu di langkah mana saja tantangan muncul. Peta ini menentukan berapa banyak solve yang dibutuhkan satu run penuh — dan karena CaptchaAI menagih per thread, bukan per solve, angka itu memengaruhi pilihan paket, bukan biaya per skenario.
| Langkah pendaftaran | Jenis CAPTCHA | Tujuan proteksi |
|---|---|---|
| Halaman login mahasiswa | reCAPTCHA v2 | Cegah percobaan kredensial otomatis |
| Pencarian / telusur mata kuliah | reCAPTCHA v2 | Batasi kueri otomatis |
| Ambil / batal mata kuliah | reCAPTCHA v2 | Cegah pengisian KRS otomatis |
| Pendaftaran daftar tunggu | reCAPTCHA v2 | Blokir bot pendaftaran |
| Perencana jadwal | Cloudflare Turnstile | Lindungi alat interaktif |
| Permintaan transkrip | reCAPTCHA v2 | Verifikasi pengguna manusia |
Kedua keluarga tantangan itu ditangani CaptchaAI: reCAPTCHA v2 lewat method userrecaptcha, Cloudflare Turnstile lewat method turnstile. Alurnya sama — kirim ke in.php, simpan task ID, polling res.php, pakai token — jadi satu helper melayani keduanya cukup dengan mengganti parameter method.
Kelas penguji Python untuk alur KRS
Kelas berikut membungkus keempat skenario inti dalam satu session requests, sehingga cookie login bertahan antar-tes. _solve_recaptcha dipanggil hanya ketika _has_captcha menemukan penanda reCAPTCHA — pengecekan bersyarat ini menghemat solve pada halaman yang kebetulan bersih.
import requests
import time
import re
class RegistrationTester:
def __init__(self, api_key, portal_url):
self.api_key = api_key
self.portal_url = portal_url
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
def test_login(self, student_id, password):
"""Test login flow with CAPTCHA handling."""
login_page = self.session.get(f"{self.portal_url}/login")
site_key = self._extract_site_key(login_page.text)
token = self._solve_recaptcha(site_key, f"{self.portal_url}/login")
response = self.session.post(f"{self.portal_url}/login", data={
"studentId": student_id,
"password": password,
"g-recaptcha-response": token
})
return {
"status": response.status_code,
"redirected": response.url != f"{self.portal_url}/login",
"success": "dashboard" in response.url or response.status_code == 200
}
def test_course_search(self, term, department, level=None):
"""Test course search returns correct results."""
params = {"term": term, "dept": department}
if level:
params["level"] = level
response = self.session.get(
f"{self.portal_url}/courses/search", params=params
)
if self._has_captcha(response.text):
site_key = self._extract_site_key(response.text)
token = self._solve_recaptcha(
site_key, f"{self.portal_url}/courses/search"
)
response = self.session.post(
f"{self.portal_url}/courses/search",
data={**params, "g-recaptcha-response": token}
)
courses = self._parse_courses(response.text)
return {
"count": len(courses),
"courses": courses,
"all_match_dept": all(
c["department"] == department for c in courses
)
}
def test_enrollment(self, course_id, section):
"""Test enrolling in a course section."""
enroll_page = self.session.get(
f"{self.portal_url}/enroll/{course_id}/{section}"
)
if self._has_captcha(enroll_page.text):
site_key = self._extract_site_key(enroll_page.text)
token = self._solve_recaptcha(
site_key,
f"{self.portal_url}/enroll/{course_id}/{section}"
)
else:
token = None
form_data = {
"courseId": course_id,
"section": section,
"confirm": "true"
}
if token:
form_data["g-recaptcha-response"] = token
response = self.session.post(
f"{self.portal_url}/enroll/submit",
data=form_data
)
return {
"status": response.status_code,
"enrolled": "success" in response.text.lower(),
"waitlisted": "waitlist" in response.text.lower(),
"error": self._extract_error(response.text)
}
def test_seat_availability(self, course_ids):
"""Test seat availability display for multiple courses."""
results = []
for course_id in course_ids:
response = self.session.get(
f"{self.portal_url}/courses/{course_id}"
)
if self._has_captcha(response.text):
site_key = self._extract_site_key(response.text)
token = self._solve_recaptcha(
site_key, f"{self.portal_url}/courses/{course_id}"
)
response = self.session.post(
f"{self.portal_url}/courses/{course_id}",
data={"g-recaptcha-response": token}
)
availability = self._parse_availability(response.text)
results.append({"course_id": course_id, **availability})
return results
def _has_captcha(self, html):
return "g-recaptcha" in html or "recaptcha" in html.lower()
def _extract_site_key(self, html):
match = re.search(r'data-sitekey="([^"]+)"', html)
if match:
return match.group(1)
raise ValueError("reCAPTCHA site key not found")
def _solve_recaptcha(self, site_key, page_url):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("reCAPTCHA solve timed out")
def _parse_courses(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
def text_or_empty(node):
return node.text.strip() if node and node.text else ""
courses = []
for row in soup.select(".course-row, tr.course"):
courses.append({
"id": row.get("data-course-id", ""),
"department": text_or_empty(row.select_one(".dept")),
"number": text_or_empty(row.select_one(".number")),
"title": text_or_empty(row.select_one(".title")),
"seats": text_or_empty(row.select_one(".seats"))
})
return courses
def _parse_availability(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
def text_or_none(node):
return node.text.strip() if node and node.text else None
return {
"total_seats": text_or_none(soup.select_one(".total-seats")),
"enrolled": text_or_none(soup.select_one(".enrolled-count")),
"available": text_or_none(soup.select_one(".available-seats")),
"waitlist": text_or_none(soup.select_one(".waitlist-count"))
}
def _extract_error(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
error = soup.select_one(".error-message, .alert-danger")
return error.text.strip() if error else None
Tiga detail sering terlewat saat mengadaptasinya ke SIAKAD tertentu. Pertama, _extract_site_key mengandalkan atribut data-sitekey pada HTML yang dirender server; portal SPA merender widget belakangan sehingga sitekey belum ada di respons awal. Kedua, pageurl harus persis sama dengan URL halaman tempat widget tampil — salah URL, token ditolak portal. Ketiga, loop polling berhenti setelah 60 percobaan berjeda 3 detik; untuk suite besar, pakai jeda yang meningkat eksponensial (exponential backoff).
Menjalankan seluruh skenario sebagai satu suite (JavaScript)
Di stack Node.js, pola yang sama bisa dijalankan sebagai runner yang mengumpulkan hasil tiap skenario dan mencetak laporan ringkas — berguna sebagai artefak build di pipeline CI.
class RegistrationTestSuite {
constructor(apiKey, portalUrl) {
this.apiKey = apiKey;
this.portalUrl = portalUrl;
this.results = [];
}
async runAll(testCredentials) {
const tests = [
() => this.testLogin(testCredentials),
() => this.testCourseSearch('Fall2025', 'CS'),
() => this.testEnrollment('CS101', '001'),
() => this.testDropCourse('CS101', '001'),
() => this.testWaitlistFlow('CS201', '001')
];
for (const test of tests) {
try {
const result = await test();
this.results.push({ ...result, passed: true });
} catch (error) {
this.results.push({ name: test.name, error: error.message, passed: false });
}
}
return this.generateReport();
}
async solveRecaptcha(pageUrl, html) {
const match = html.match(/data-sitekey="([^"]+)"/);
if (!match) return null;
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: new URLSearchParams({
key: this.apiKey,
method: 'userrecaptcha',
googlekey: match[1],
pageurl: pageUrl,
json: '1'
})
});
const { request: taskId } = await submitResp.json();
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 3000));
const result = await fetch(
`https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
);
const data = await result.json();
if (data.status === 1) return data.request;
}
throw new Error('reCAPTCHA solve timed out');
}
generateReport() {
const passed = this.results.filter(r => r.passed).length;
const failed = this.results.filter(r => !r.passed).length;
return {
total: this.results.length,
passed,
failed,
passRate: `${((passed / this.results.length) * 100).toFixed(1)}%`,
details: this.results
};
}
}
// Usage
const suite = new RegistrationTestSuite('YOUR_API_KEY', 'https://register.university.edu');
const report = await suite.runAll({ studentId: 'test123', password: 'testpass' });
console.log(`Tests: ${report.passed}/${report.total} passed (${report.passRate})`);
Runner ini menangkap kesalahan per skenario alih-alih menghentikan seluruh run, sehingga satu tes daftar tunggu yang gagal tidak menyembunyikan hasil tes login. Untuk perilaku token pada widget yang memakai fungsi callback, lihat cara menangani callback reCAPTCHA v2 lewat API dan penjelasan mekanisme callback reCAPTCHA v2.
Skenario mana yang paling banyak memakan solve
Tidak semua tes memakan anggaran solve yang sama. Skenario di baris atas menghabiskan solve jauh lebih cepat daripada baris bawah.
| Skenario pengujian | Frekuensi CAPTCHA | Dampak pada suite |
|---|---|---|
| Login banyak akun mahasiswa uji | Tinggi | Hampir setiap percobaan login memunculkan tantangan |
| Pengambilan KRS bersamaan | Tinggi | Proteksi anti-bot aktif penuh |
| Pencarian mata kuliah beruntun | Sedang | Kueri terkena pembatasan laju permintaan (rate limiting) |
| Pembuatan jadwal | Rendah | Interaksi dalam satu sesi |
| Validasi prasyarat | Rendah | Bagian dari alur pengambilan KRS |
Karena paket CaptchaAI dihitung per thread bersamaan dengan solve tak terbatas per thread, yang perlu diperkirakan adalah jumlah solve yang berjalan serentak, bukan total sebulan. Suite QA satu tim biasanya cukup di BASIC ($15/bulan, 5 thread) atau STANDARD ($30/bulan, 15 thread); uji beban ratusan sesi mahasiswa sekaligus baru masuk ke ADVANCE ($90/bulan, 50 thread) ke atas. Cek halaman pricing CaptchaAI untuk daftar tier terbaru.
Masalah yang muncul saat suite dijalankan
| Gejala | Penyebab | Penanganan |
|---|---|---|
| Tantangan muncul di setiap tes login | Sesi tidak dipakai ulang | Autentikasi sekali, jalankan seluruh tes dalam satu session |
| Pengambilan KRS gagal padahal token valid | Token CSRF sudah kedaluwarsa | Ambil token CSRF baru bersamaan dengan solve CAPTCHA |
| Data uji hilang antar-run | Portal membersihkan KRS uji | Pakai lingkungan pengujian khusus, bukan produksi |
sitekey reCAPTCHA tidak ditemukan |
Portal SPA merender widget secara dinamis | Pakai otomatisasi browser untuk membaca DOM setelah render |
| Token ditolak portal | pageurl tidak sama dengan URL halaman widget |
Kirim URL halaman yang persis, termasuk path lengkap |
Untuk portal yang memakai reCAPTCHA v2 di halaman login dan Turnstile di perencana jadwal, baca penanganan Turnstile dan reCAPTCHA v2 di situs yang sama agar helper Anda memilih method yang tepat per widget.
Pertanyaan umum
Apakah pola ini boleh dipakai pada portal kampus mana pun?
Tidak. Gunakan hanya pada sistem milik institusi Anda atau yang Anda punya izin uji tertulis, dengan akun dan data uji sintetis. Menguji portal pihak ketiga tanpa izin bukan skenario QA.
Berapa lama satu solve reCAPTCHA v2 memperlambat sebuah tes?
Tambahan waktunya adalah durasi penyelesaian ditambah interval polling Anda. Ukur baseline tes tanpa CAPTCHA dulu, lalu tetapkan batas waktu suite dengan menyertakan selisih itu agar tes tidak gagal hanya karena kehabisan waktu.
Bagaimana kalau portal kampus memakai hCaptcha, bukan reCAPTCHA v2?
hCaptcha dan FunCaptcha tidak didukung CaptchaAI saat ini, dan GeeTest v4 berstatus segera hadir. Jadwalkan skenario itu sebagai tes manual, lalu otomatiskan sisa alurnya.
Bagaimana menjalankannya di pipeline CI/CD kampus?
Simpan API key sebagai secret pipeline, bukan di repository, lalu baca lewat environment variable. Tambahkan CaptchaAI sebagai dependensi tahap pengujian, dan batasi job paralel sesuai jumlah thread paket Anda supaya antrean solve tidak menumpuk.
Bisakah runner ini dipakai untuk uji beban masa KRS?
Bisa, dengan catatan throughput dibatasi jumlah thread paket Anda, bukan jumlah worker di skrip. Jalankan job di region terdekat — AWS ap-southeast-3 (Jakarta) atau ap-southeast-1 (Singapura) — supaya latensi jaringan tidak bercampur dengan waktu penyelesaian CAPTCHA.
Artikel Terkait
- Cara menangani callback reCAPTCHA v2 lewat API
- Turnstile dan reCAPTCHA v2 di satu situs
- Mekanisme callback reCAPTCHA v2
Langkah Selanjutnya
Petakan titik CAPTCHA di portal Anda, lalu ambil API key CaptchaAI dan jalankan skenario KRS pertama dari awal sampai akhir tanpa satu klik manual.