Captchas exist to stop automated interaction, which puts them in direct tension with test automation. The security feature built to block bots also blocks your legitimate tests. This guide covers practical ways to handle captchas when testing your own applications. It's about legitimate testing strategies, not getting around captchas on third-party sites. If you control the application under test, you have options. If you don't, the captcha is doing exactly what it should.
Understanding the Selenium Captcha Challenge
Why Captchas Block Selenium Automation
Captchas (Completely Automated Public Turing tests to tell Computers and Humans Apart) are designed to tell people and automated scripts apart. Modern captchas analyse:
Mouse movement patterns
Keyboard timing
Browser fingerprints
Behavioural signals
Request patterns
Selenium automation behaves in ways captcha systems recognise as machine-driven. That isn't a flaw in captchas. It's what they're for.
Why Testers Struggle with CAPTCHAs
When you test your own application and it's protected by a captcha:
Production captchas block test automation
Removing captchas for testing makes test and production environments differ
Skipping captcha-protected flows leaves gaps in coverage
Testing captcha flows by hand doesn't scale
The answer lies in how you design and configure your application, not in technical workarounds.
Types of CAPTCHAs You'll Encounter
Before choosing a strategy, it helps to know which captcha types your tests might meet. Each one detects bots differently, and each has its own testing approach.
reCAPTCHA v2 (Checkbox and Image Challenge)
Google's reCAPTCHA v2 shows the familiar "I'm not a robot" checkbox. Ticking it triggers a background check of the user's behaviour. If the system is still unsure, it shows image challenges, asking users to pick out traffic lights, crossings or bicycles.
Detection looks at:
Mouse movement before and during the click
How long the user spent on the page before interacting
The browser environment and plugin fingerprint
Past behaviour linked to the IP address
Cookies from previous Google interactions
reCAPTCHA v2 is hard to automate, because even reaching the checkbox programmatically raises suspicion, and the image challenges are designed specifically to defeat automated solving.
Testing approach
Use Google's official test keys in test environments. These keys bypass all verification and always return success.
reCAPTCHA v3 (Invisible Score-Based)
reCAPTCHA v3 runs entirely in the background, with no user interaction. Instead of showing challenges, it monitors behaviour and returns a score between 0.0 and 1.0, where 1.0 means it's highly confident the user is human.
The score is based on:
Mouse movement and scrolling
Typing rhythm
How the user navigates between pages
Time between actions
Browser and device consistency
Your application decides what to do with each score. A score below 0.5 might trigger extra verification, and below 0.3 might block the action completely.
Testing approach
Configure your backend to accept every score in test environments, or mock the response so it always returns 1.0. Note that Google's published test keys only work with reCAPTCHA v2. For v3, register a separate key for your test environment and bypass or mock the score check on your server:
import os
import requests
SITEVERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
def verify_recaptcha_v3(token: str, min_score: float = 0.5) -> bool:
if os.getenv("APP_ENV") == "test":
return True
resp = requests.post(
SITEVERIFY_URL,
data={"secret": os.environ["RECAPTCHA_SECRET"], "response": token},
timeout=5,
)
result = resp.json()
return result.get("success", False) and result.get("score", 0) >= min_scoreimport os
import requests
SITEVERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
def verify_recaptcha_v3(token: str, min_score: float = 0.5) -> bool:
if os.getenv("APP_ENV") == "test":
return True
resp = requests.post(
SITEVERIFY_URL,
data={"secret": os.environ["RECAPTCHA_SECRET"], "response": token},
timeout=5,
)
result = resp.json()
return result.get("success", False) and result.get("score", 0) >= min_scoreimport os
import requests
SITEVERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
def verify_recaptcha_v3(token: str, min_score: float = 0.5) -> bool:
if os.getenv("APP_ENV") == "test":
return True
resp = requests.post(
SITEVERIFY_URL,
data={"secret": os.environ["RECAPTCHA_SECRET"], "response": token},
timeout=5,
)
result = resp.json()
return result.get("success", False) and result.get("score", 0) >= min_scoreimport os
import requests
SITEVERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
def verify_recaptcha_v3(token: str, min_score: float = 0.5) -> bool:
if os.getenv("APP_ENV") == "test":
return True
resp = requests.post(
SITEVERIFY_URL,
data={"secret": os.environ["RECAPTCHA_SECRET"], "response": token},
timeout=5,
)
result = resp.json()
return result.get("success", False) and result.get("score", 0) >= min_scorehCaptcha
hCaptcha started as a privacy-focused alternative to reCAPTCHA. It uses image classification challenges similar to reCAPTCHA v2, but with different images and detection models.
How it differs from reCAPTCHA:
It doesn't rely on Google's tracking infrastructure
It offers site owners compensation for using the service
It uses different machine learning models to detect bots
It provides accessibility options, including audio challenges
Testing approach
hCaptcha provides test keys, just like Google:
Test site key: 10000000-ffff-ffff-ffff-000000000001
Test secret key: 0x0000000000000000000000000000000000000000
These keys always pass verification:
<div class="h-captcha" data-sitekey="10000000-ffff-ffff-ffff-000000000001"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="10000000-ffff-ffff-ffff-000000000001"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="10000000-ffff-ffff-ffff-000000000001"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="10000000-ffff-ffff-ffff-000000000001"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
Image-Based CAPTCHAs
Traditional image captchas show distorted text or numbers that users have to type correctly. They're less common on modern sites, but still appear in legacy applications and government portals.
Variations include:
Distorted letters and numbers
Maths problems shown as images
Pattern recognition challenges
Drag-and-drop puzzle pieces
These captchas have no standard test mode, so your options depend on whether you control the implementation.
Testing approach
For applications you control, accept a fixed test answer in test environments:
import os
TEST_CAPTCHA_ANSWER = "TEST123"
def check_image_captcha(user_answer: str, expected_answer: str) -> bool:
if os.getenv("APP_ENV") == "test" and user_answer == TEST_CAPTCHA_ANSWER:
return True
return user_answer.strip().upper() == expected_answer.upper()import os
TEST_CAPTCHA_ANSWER = "TEST123"
def check_image_captcha(user_answer: str, expected_answer: str) -> bool:
if os.getenv("APP_ENV") == "test" and user_answer == TEST_CAPTCHA_ANSWER:
return True
return user_answer.strip().upper() == expected_answer.upper()import os
TEST_CAPTCHA_ANSWER = "TEST123"
def check_image_captcha(user_answer: str, expected_answer: str) -> bool:
if os.getenv("APP_ENV") == "test" and user_answer == TEST_CAPTCHA_ANSWER:
return True
return user_answer.strip().upper() == expected_answer.upper()import os
TEST_CAPTCHA_ANSWER = "TEST123"
def check_image_captcha(user_answer: str, expected_answer: str) -> bool:
if os.getenv("APP_ENV") == "test" and user_answer == TEST_CAPTCHA_ANSWER:
return True
return user_answer.strip().upper() == expected_answer.upper()Audio CAPTCHAs
Audio captchas are the accessible alternative to visual challenges. Users listen to spoken characters or words and type what they hear, with background noise and distortion making automated transcription harder.
They include:
reCAPTCHA's audio alternative
hCaptcha audio challenges
Custom audio verification systems
Speech-to-text tools increasingly target audio captchas, so providers keep adding more distortion and noise.
Testing approach
The test keys that skip visual captchas usually skip the audio alternatives too, so you rarely need separate configuration.
Headless Browser Detection
Running Selenium in headless mode amplifies CAPTCHA detection. Headless browsers lack visual rendering components that leave distinctive fingerprints, and CAPTCHA systems specifically check for these anomalies.
Why Headless Triggers More CAPTCHAs
Headless browsers differ from normal browsers in ways that are easy to detect.
Navigator properties:
Other differences:
Missing WebGL renderer information
An empty or minimal plugin list
Inconsistent screen dimensions
No mouse movement before clicks
Perfectly consistent timing between actions
What This Means for Your Test Strategy
Bypassing captchas in test environments is essential: Headless automation triggers captchas more often, so trying to avoid detection isn't reliable.
Use headed mode for debugging: When investigating a captcha-related failure, run the browser visibly so you can see what's happening.
Don't rely on stealth techniques: Libraries claim to hide automation fingerprints, but captcha providers keep updating their detection. Configuring your own application is far more reliable.
Proven Strategies to Bypass Captchas in Your Own Applications
These strategies apply when you control the application under test. They involve configuring your own systems to support testing, not defeating security measures.
Strategy 1: Turn Off Captcha Verification in Test Environments
Configure test environments to skip captcha verification, while keeping captchas fully active in production.
Environment Variable Control
Let an environment setting decide whether captcha is required:
import os
def captcha_required() -> bool:
if os.getenv("APP_ENV") == "production":
return True
return os.getenv("DISABLE_CAPTCHA", "false").lower() != "true"import os
def captcha_required() -> bool:
if os.getenv("APP_ENV") == "production":
return True
return os.getenv("DISABLE_CAPTCHA", "false").lower() != "true"import os
def captcha_required() -> bool:
if os.getenv("APP_ENV") == "production":
return True
return os.getenv("DISABLE_CAPTCHA", "false").lower() != "true"import os
def captcha_required() -> bool:
if os.getenv("APP_ENV") == "production":
return True
return os.getenv("DISABLE_CAPTCHA", "false").lower() != "true"Test environments set DISABLE_CAPTCHA=true. Production ignores it entirely, so captchas stay active for real users.
Test Mode Configuration
Many captcha providers offer test modes that always pass. Google's reCAPTCHA v2 test keys are:
Site key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
Secret key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
They return a successful verification whatever the user does. Use them in test environments only:
<div class="g-recaptcha" data-sitekey="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
Feature Flags
Use feature flags to control captcha enforcement:
enforce-captcha: false
enforce-captcha: true
enforce-captcha: false
enforce-captcha: true
enforce-captcha: false
enforce-captcha: true
enforce-captcha: false
enforce-captcha: true
def should_enforce_captcha(flags) -> bool:
return flags.is_enabled("enforce-captcha", default=True)def should_enforce_captcha(flags) -> bool:
return flags.is_enabled("enforce-captcha", default=True)def should_enforce_captcha(flags) -> bool:
return flags.is_enabled("enforce-captcha", default=True)def should_enforce_captcha(flags) -> bool:
return flags.is_enabled("enforce-captcha", default=True)Turn the flag off in test environments and keep it on in production. Feature flags give you flexibility and a record of when settings changed.
Strategy 2: API Level Testing
Where it makes sense, skip the UI captcha entirely by testing through the API.
Direct API testing
Many captcha-protected flows can be tested through APIs that accept authentication directly:
import os
import requests
def test_login_api_accepts_valid_credentials():
resp = requests.post(
"https://test-api.example.com/auth/login",
json={
"email": "qa.user@test.example.com",
"password": os.environ["QA_USER_PASSWORD"],
},
timeout=10,
)
assert resp.status_code == 200
assert "access_token" in resp.json()import os
import requests
def test_login_api_accepts_valid_credentials():
resp = requests.post(
"https://test-api.example.com/auth/login",
json={
"email": "qa.user@test.example.com",
"password": os.environ["QA_USER_PASSWORD"],
},
timeout=10,
)
assert resp.status_code == 200
assert "access_token" in resp.json()import os
import requests
def test_login_api_accepts_valid_credentials():
resp = requests.post(
"https://test-api.example.com/auth/login",
json={
"email": "qa.user@test.example.com",
"password": os.environ["QA_USER_PASSWORD"],
},
timeout=10,
)
assert resp.status_code == 200
assert "access_token" in resp.json()import os
import requests
def test_login_api_accepts_valid_credentials():
resp = requests.post(
"https://test-api.example.com/auth/login",
json={
"email": "qa.user@test.example.com",
"password": os.environ["QA_USER_PASSWORD"],
},
timeout=10,
)
assert resp.status_code == 200
assert "access_token" in resp.json()This tests the authentication logic without fighting the captcha.
Pre-authenticated sessions
For UI tests that need a signed-in user, sign in through the API and pass the session to the browser:
import os
import requests
def open_dashboard_as_logged_in_user(driver):
resp = requests.post(
"https://test-api.example.com/auth/login",
json={
"email": "qa.user@test.example.com",
"password": os.environ["QA_USER_PASSWORD"],
},
timeout=10,
)
session_id = resp.cookies.get("session_id")
driver.get("https://test.example.com")
driver.add_cookie({
"name": "session_id",
"value": session_id,
"path": "/",
"secure": True,
"httpOnly": True,
})
driver.get("https://test.example.com/dashboard")import os
import requests
def open_dashboard_as_logged_in_user(driver):
resp = requests.post(
"https://test-api.example.com/auth/login",
json={
"email": "qa.user@test.example.com",
"password": os.environ["QA_USER_PASSWORD"],
},
timeout=10,
)
session_id = resp.cookies.get("session_id")
driver.get("https://test.example.com")
driver.add_cookie({
"name": "session_id",
"value": session_id,
"path": "/",
"secure": True,
"httpOnly": True,
})
driver.get("https://test.example.com/dashboard")import os
import requests
def open_dashboard_as_logged_in_user(driver):
resp = requests.post(
"https://test-api.example.com/auth/login",
json={
"email": "qa.user@test.example.com",
"password": os.environ["QA_USER_PASSWORD"],
},
timeout=10,
)
session_id = resp.cookies.get("session_id")
driver.get("https://test.example.com")
driver.add_cookie({
"name": "session_id",
"value": session_id,
"path": "/",
"secure": True,
"httpOnly": True,
})
driver.get("https://test.example.com/dashboard")import os
import requests
def open_dashboard_as_logged_in_user(driver):
resp = requests.post(
"https://test-api.example.com/auth/login",
json={
"email": "qa.user@test.example.com",
"password": os.environ["QA_USER_PASSWORD"],
},
timeout=10,
)
session_id = resp.cookies.get("session_id")
driver.get("https://test.example.com")
driver.add_cookie({
"name": "session_id",
"value": session_id,
"path": "/",
"secure": True,
"httpOnly": True,
})
driver.get("https://test.example.com/dashboard")Strategy 3: Test User Allowlists
Configure your application to skip captcha for designated test accounts.
Email Domain Allowlisting
Skip captcha for email addresses on a dedicated test domain:
import os
TEST_EMAIL_DOMAINS = {"test.example.com"}
def captcha_required_for(email: str) -> bool:
if os.getenv("APP_ENV") == "production":
return True
domain = email.rsplit("@", 1)[-1].lower()
return domain not in TEST_EMAIL_DOMAINSimport os
TEST_EMAIL_DOMAINS = {"test.example.com"}
def captcha_required_for(email: str) -> bool:
if os.getenv("APP_ENV") == "production":
return True
domain = email.rsplit("@", 1)[-1].lower()
return domain not in TEST_EMAIL_DOMAINSimport os
TEST_EMAIL_DOMAINS = {"test.example.com"}
def captcha_required_for(email: str) -> bool:
if os.getenv("APP_ENV") == "production":
return True
domain = email.rsplit("@", 1)[-1].lower()
return domain not in TEST_EMAIL_DOMAINSimport os
TEST_EMAIL_DOMAINS = {"test.example.com"}
def captcha_required_for(email: str) -> bool:
if os.getenv("APP_ENV") == "production":
return True
domain = email.rsplit("@", 1)[-1].lower()
return domain not in TEST_EMAIL_DOMAINSTest accounts use addresses like testuser@test.example.com. Only use this in test and staging environments. In production, anyone can type an address on your test domain, so an email allowlist would let attackers skip the captcha entirely.
IP Address Allowlisting
Skip captcha for requests from known test infrastructure IPs:
import ipaddress
TEST_RUNNER_NETWORKS = [ipaddress.ip_network("203.0.113.0/28")]
def captcha_required_for_ip(remote_addr: str) -> bool:
ip = ipaddress.ip_address(remote_addr)
return not any(ip in network for network in TEST_RUNNER_NETWORKS)import ipaddress
TEST_RUNNER_NETWORKS = [ipaddress.ip_network("203.0.113.0/28")]
def captcha_required_for_ip(remote_addr: str) -> bool:
ip = ipaddress.ip_address(remote_addr)
return not any(ip in network for network in TEST_RUNNER_NETWORKS)import ipaddress
TEST_RUNNER_NETWORKS = [ipaddress.ip_network("203.0.113.0/28")]
def captcha_required_for_ip(remote_addr: str) -> bool:
ip = ipaddress.ip_address(remote_addr)
return not any(ip in network for network in TEST_RUNNER_NETWORKS)import ipaddress
TEST_RUNNER_NETWORKS = [ipaddress.ip_network("203.0.113.0/28")]
def captcha_required_for_ip(remote_addr: str) -> bool:
ip = ipaddress.ip_address(remote_addr)
return not any(ip in network for network in TEST_RUNNER_NETWORKS)This works well for CI/CD environments with static IPs. Always check the IP from the actual connection, or from a proxy you control. Never trust a header like X-Forwarded-For set by the client, because it's easy to fake.
Strategy 4: Captcha Isolation
Separate the captcha from the flows you need to test.
Separate Captcha Testing
Check that the captcha is present on its own, and test the rest of the flow without it.
Captcha presence test (automated, or as part of a visual check):
from selenium.webdriver.common.by import By
def test_captcha_widget_is_present_on_signup(driver):
driver.get("https://staging.example.com/signup")
widgets = driver.find_elements(By.CSS_SELECTOR, "iframe[title='reCAPTCHA']")
assert len(widgets) == 1, "Captcha widget should be present on the signup page"from selenium.webdriver.common.by import By
def test_captcha_widget_is_present_on_signup(driver):
driver.get("https://staging.example.com/signup")
widgets = driver.find_elements(By.CSS_SELECTOR, "iframe[title='reCAPTCHA']")
assert len(widgets) == 1, "Captcha widget should be present on the signup page"from selenium.webdriver.common.by import By
def test_captcha_widget_is_present_on_signup(driver):
driver.get("https://staging.example.com/signup")
widgets = driver.find_elements(By.CSS_SELECTOR, "iframe[title='reCAPTCHA']")
assert len(widgets) == 1, "Captcha widget should be present on the signup page"from selenium.webdriver.common.by import By
def test_captcha_widget_is_present_on_signup(driver):
driver.get("https://staging.example.com/signup")
widgets = driver.find_elements(By.CSS_SELECTOR, "iframe[title='reCAPTCHA']")
assert len(widgets) == 1, "Captcha widget should be present on the signup page"Flow tests that skip the captcha:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def test_signup_flow(driver):
driver.get("https://test.example.com/signup")
driver.find_element(By.NAME, "email").send_keys("new.user@test.example.com")
driver.find_element(By.NAME, "password").send_keys("S3cure!pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
message = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".signup-success"))
)
assert "Check your inbox" in message.textfrom selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def test_signup_flow(driver):
driver.get("https://test.example.com/signup")
driver.find_element(By.NAME, "email").send_keys("new.user@test.example.com")
driver.find_element(By.NAME, "password").send_keys("S3cure!pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
message = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".signup-success"))
)
assert "Check your inbox" in message.textfrom selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def test_signup_flow(driver):
driver.get("https://test.example.com/signup")
driver.find_element(By.NAME, "email").send_keys("new.user@test.example.com")
driver.find_element(By.NAME, "password").send_keys("S3cure!pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
message = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".signup-success"))
)
assert "Check your inbox" in message.textfrom selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def test_signup_flow(driver):
driver.get("https://test.example.com/signup")
driver.find_element(By.NAME, "email").send_keys("new.user@test.example.com")
driver.find_element(By.NAME, "password").send_keys("S3cure!pass")
driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
message = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".signup-success"))
)
assert "Check your inbox" in message.textThis confirms the captcha appears in the UI, while still letting the flow be tested automatically.
Mock Captcha Responses
In test environments, accept a known mock token:
import os
MOCK_CAPTCHA_TOKEN = "test-captcha-token"
def verify_captcha(token: str) -> bool:
if os.getenv("APP_ENV") == "test" and token == MOCK_CAPTCHA_TOKEN:
return True
return verify_with_provider(token)import os
MOCK_CAPTCHA_TOKEN = "test-captcha-token"
def verify_captcha(token: str) -> bool:
if os.getenv("APP_ENV") == "test" and token == MOCK_CAPTCHA_TOKEN:
return True
return verify_with_provider(token)import os
MOCK_CAPTCHA_TOKEN = "test-captcha-token"
def verify_captcha(token: str) -> bool:
if os.getenv("APP_ENV") == "test" and token == MOCK_CAPTCHA_TOKEN:
return True
return verify_with_provider(token)import os
MOCK_CAPTCHA_TOKEN = "test-captcha-token"
def verify_captcha(token: str) -> bool:
if os.getenv("APP_ENV") == "test" and token == MOCK_CAPTCHA_TOKEN:
return True
return verify_with_provider(token)Then submit the mock token from your test:
driver.execute_script(
"document.querySelector('[name=\"g-recaptcha-response\"]').value = arguments[0];",
"test-captcha-token",
)
driver.execute_script(
"document.querySelector('[name=\"g-recaptcha-response\"]').value = arguments[0];",
"test-captcha-token",
)
driver.execute_script(
"document.querySelector('[name=\"g-recaptcha-response\"]').value = arguments[0];",
"test-captcha-token",
)
driver.execute_script(
"document.querySelector('[name=\"g-recaptcha-response\"]').value = arguments[0];",
"test-captcha-token",
)Your backend accepts the mock token in test environments only.
Selenium Configuration for Captcha Test Environments
Avoid configuring Selenium to hide that it's automated. Captcha providers update their detection constantly, so stealth settings break without warning, and they're the same techniques used to get past captchas on sites you don't own. Instead, configure Selenium to work with your test environment's captcha settings.
Run headed when investigating captcha failures
import os
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
# Run headless in CI, but headed when debugging captcha behaviour
if os.getenv("DEBUG_HEADED") != "true":
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)import os
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
# Run headless in CI, but headed when debugging captcha behaviour
if os.getenv("DEBUG_HEADED") != "true":
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)import os
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
# Run headless in CI, but headed when debugging captcha behaviour
if os.getenv("DEBUG_HEADED") != "true":
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)import os
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1920,1080")
# Run headless in CI, but headed when debugging captcha behaviour
if os.getenv("DEBUG_HEADED") != "true":
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)Interact with the widget when it uses test keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(driver, 10).until(
EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, "iframe[title='reCAPTCHA']"))
)
driver.find_element(By.ID, "recaptcha-anchor").click()
driver.switch_to.default_content()from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(driver, 10).until(
EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, "iframe[title='reCAPTCHA']"))
)
driver.find_element(By.ID, "recaptcha-anchor").click()
driver.switch_to.default_content()from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(driver, 10).until(
EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, "iframe[title='reCAPTCHA']"))
)
driver.find_element(By.ID, "recaptcha-anchor").click()
driver.switch_to.default_content()from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
WebDriverWait(driver, 10).until(
EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, "iframe[title='reCAPTCHA']"))
)
driver.find_element(By.ID, "recaptcha-anchor").click()
driver.switch_to.default_content()What About Third-Party Applications?
If you don't control the application, your options are limited by design.
Why Getting Around Captchas is a Problem
Trying to get past captchas on applications you don't control:
Is likely to break the application's terms of service
May break computer access laws in some countries
Weakens security for the application's genuine users
Often relies on third-party solving services that raise ethical and legal concerns
Legitimate Alternatives
Ask for test access: Contact the application owner about a test environment, or test accounts with lighter security.
Use their APIs: Many applications offer APIs for legitimate integrations that don't need a captcha.
Scope your testing: Accept that some third-party integrations can't be fully automated, and focus on testing what you control.
Check critical flows by hand: For the third-party flows that matter most, build manual verification into your process.
Best Practices for Building CAPTCHA-Testable Applications
Design for Testability
When building applications, design the captcha integration so it's easy to test.
Separate captcha from business logic:
from abc import ABC, abstractmethod
import os
import requests
class CaptchaVerifier(ABC):
@abstractmethod
def verify(self, token: str, remote_ip: str) -> bool:
...
class RecaptchaVerifier(CaptchaVerifier):
def verify(self, token: str, remote_ip: str) -> bool:
resp = requests.post(
"https://www.google.com/recaptcha/api/siteverify",
data={
"secret": os.environ["RECAPTCHA_SECRET"],
"response": token,
"remoteip": remote_ip,
},
timeout=5,
)
return resp.json().get("success", False)
class SignupService:
def __init__(self, captcha: CaptchaVerifier):
self.captcha = captcha
def register(self, email: str, password: str, captcha_token: str, remote_ip: str):
if not self.captcha.verify(captcha_token, remote_ip):
raise PermissionError("Captcha verification failed")
from abc import ABC, abstractmethod
import os
import requests
class CaptchaVerifier(ABC):
@abstractmethod
def verify(self, token: str, remote_ip: str) -> bool:
...
class RecaptchaVerifier(CaptchaVerifier):
def verify(self, token: str, remote_ip: str) -> bool:
resp = requests.post(
"https://www.google.com/recaptcha/api/siteverify",
data={
"secret": os.environ["RECAPTCHA_SECRET"],
"response": token,
"remoteip": remote_ip,
},
timeout=5,
)
return resp.json().get("success", False)
class SignupService:
def __init__(self, captcha: CaptchaVerifier):
self.captcha = captcha
def register(self, email: str, password: str, captcha_token: str, remote_ip: str):
if not self.captcha.verify(captcha_token, remote_ip):
raise PermissionError("Captcha verification failed")
from abc import ABC, abstractmethod
import os
import requests
class CaptchaVerifier(ABC):
@abstractmethod
def verify(self, token: str, remote_ip: str) -> bool:
...
class RecaptchaVerifier(CaptchaVerifier):
def verify(self, token: str, remote_ip: str) -> bool:
resp = requests.post(
"https://www.google.com/recaptcha/api/siteverify",
data={
"secret": os.environ["RECAPTCHA_SECRET"],
"response": token,
"remoteip": remote_ip,
},
timeout=5,
)
return resp.json().get("success", False)
class SignupService:
def __init__(self, captcha: CaptchaVerifier):
self.captcha = captcha
def register(self, email: str, password: str, captcha_token: str, remote_ip: str):
if not self.captcha.verify(captcha_token, remote_ip):
raise PermissionError("Captcha verification failed")
from abc import ABC, abstractmethod
import os
import requests
class CaptchaVerifier(ABC):
@abstractmethod
def verify(self, token: str, remote_ip: str) -> bool:
...
class RecaptchaVerifier(CaptchaVerifier):
def verify(self, token: str, remote_ip: str) -> bool:
resp = requests.post(
"https://www.google.com/recaptcha/api/siteverify",
data={
"secret": os.environ["RECAPTCHA_SECRET"],
"response": token,
"remoteip": remote_ip,
},
timeout=5,
)
return resp.json().get("success", False)
class SignupService:
def __init__(self, captcha: CaptchaVerifier):
self.captcha = captcha
def register(self, email: str, password: str, captcha_token: str, remote_ip: str):
if not self.captcha.verify(captcha_token, remote_ip):
raise PermissionError("Captcha verification failed")
Inject a mock verifier during testing:
class AlwaysPassVerifier(CaptchaVerifier):
def verify(self, token: str, remote_ip: str) -> bool:
return True
def test_register_creates_account():
service = SignupService(captcha=AlwaysPassVerifier())
service.register("jane@test.example.com", "S3cure!pass", "any-token", "127.0.0.1")
class AlwaysPassVerifier(CaptchaVerifier):
def verify(self, token: str, remote_ip: str) -> bool:
return True
def test_register_creates_account():
service = SignupService(captcha=AlwaysPassVerifier())
service.register("jane@test.example.com", "S3cure!pass", "any-token", "127.0.0.1")
class AlwaysPassVerifier(CaptchaVerifier):
def verify(self, token: str, remote_ip: str) -> bool:
return True
def test_register_creates_account():
service = SignupService(captcha=AlwaysPassVerifier())
service.register("jane@test.example.com", "S3cure!pass", "any-token", "127.0.0.1")
class AlwaysPassVerifier(CaptchaVerifier):
def verify(self, token: str, remote_ip: str) -> bool:
return True
def test_register_creates_account():
service = SignupService(captcha=AlwaysPassVerifier())
service.register("jane@test.example.com", "S3cure!pass", "any-token", "127.0.0.1")
Manage the Gap Between Environments
Turning captcha off in test environments means they no longer match production exactly. Reduce that risk by:
Running periodic manual tests through the full captcha flow
Monitoring captcha metrics in production
Testing that the captcha is present, even where verification is skipped
Including the captcha in performance testing
Document Your Approach
Write down your captcha testing strategy, including:
Which environments have captcha turned off
How to switch on test mode
Which test accounts skip the captcha
Which captcha flows are covered by manual testing
This avoids confusion and makes sure nothing falls through the gaps.
AI Native Testing Approach to Handle Captcha Scenarios
AI native test platforms handle captcha scenarios through the same architectural approaches, but with additional benefits.
Natural Language Test Design
Describe the flow without building captcha handling into the test:
The test describes the flow. Environment configuration decides whether the captcha appears and how it behaves.
One Test, Every Environment
The same plain English test can run anywhere:
Test environment: captcha is bypassed, and the test passes automatically
Staging environment: captcha runs in test mode, and verification succeeds
Production verification: the test is run manually, with a person completing the captcha
No test changes are needed. Configuration handles the differences.
API and UI Unified Testing
Platfom like Virtuoso QA combines API and UI testing in single journeys. Use API authentication to bypass captcha protected login, then continue with UI testing:
This uses the API where it's most efficient, while keeping full UI coverage for everything after login.
Design for Testing From the Start
Handling captchas in test automation is an architecture problem, not a tooling problem. No framework or AI can legitimately get past a captcha designed to stop automation. The answer is designing your applications so they can be tested through configuration.
When you control the application:
Turn off captcha verification in test environments
Use the provider's test modes and keys
Allowlist test users in non-production environments
Keep captcha logic separate from business logic
Document your testing approach
When you don't control the application:
Ask the owner for test access
Use the APIs they provide
Accept that some automation coverage will be limited
Check critical flows by hand
Virtuoso QA supports these approaches through:
Release evidence: Every run records which environment it ran against, what happened at each step, and who approved the test, so you can show exactly where the captcha was bypassed and where it was tested in full.
Natural Language Programming: Describe flows independently of how the captcha is configured.
API and UI in one journey: Sign in through the API, then continue testing in the UI.
Environment configuration: Run the same tests across environments with different captcha settings.
Flexible execution: Adapt to differences between environments without changing the tests.
The goal is thorough testing of your own applications, not defeating anyone's security.
Related Reads