Blog

No More Captcha Headaches - How to Handle Captcha in Selenium Testing

Rishabh Kumar

Published on

Table of contents

Lorem ipsum

Lorem ipsum

Lorem ipsum

Lorem ipsum

Lorem ipsum

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:
    # Test environments accept every request as human
    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_score
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:
    # Test environments accept every request as human
    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_score
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:
    # Test environments accept every request as human
    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_score
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:
    # Test environments accept every request as human
    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_score

hCaptcha

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:

<!-- Test environments only: this key always passes 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>
<!-- Test environments only: this key always passes 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>
<!-- Test environments only: this key always passes 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>
<!-- Test environments only: this key always passes 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>

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:
    # Accept a fixed answer in test environments only
    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:
    # Accept a fixed answer in test environments only
    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:
    # Accept a fixed answer in test environments only
    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:
    # Accept a fixed answer in test environments only
    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:
    # Fail safe: production always enforces captcha, whatever the variable says
    if os.getenv("APP_ENV") == "production":
        return True
    return os.getenv("DISABLE_CAPTCHA", "false").lower() != "true"
import os


def captcha_required() -> bool:
    # Fail safe: production always enforces captcha, whatever the variable says
    if os.getenv("APP_ENV") == "production":
        return True
    return os.getenv("DISABLE_CAPTCHA", "false").lower() != "true"
import os


def captcha_required() -> bool:
    # Fail safe: production always enforces captcha, whatever the variable says
    if os.getenv("APP_ENV") == "production":
        return True
    return os.getenv("DISABLE_CAPTCHA", "false").lower() != "true"
import os


def captcha_required() -> bool:
    # Fail safe: production always enforces captcha, whatever the variable says
    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:

<!-- Google's official reCAPTCHA v2 test site key: test environments only -->
<div class="g-recaptcha" data-sitekey="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<!-- Google's official reCAPTCHA v2 test site key: test environments only -->
<div class="g-recaptcha" data-sitekey="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<!-- Google's official reCAPTCHA v2 test site key: test environments only -->
<div class="g-recaptcha" data-sitekey="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<!-- Google's official reCAPTCHA v2 test site key: test environments only -->
<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:

# flags.test.yaml
enforce-captcha: false

# flags.production.yaml
enforce-captcha: true
# flags.test.yaml
enforce-captcha: false

# flags.production.yaml
enforce-captcha: true
# flags.test.yaml
enforce-captcha: false

# flags.production.yaml
enforce-captcha: true
# flags.test.yaml
enforce-captcha: false

# flags.production.yaml
enforce-captcha: true
def should_enforce_captcha(flags) -> bool:
    # Defaults to enforcing if the flag is missing
    return flags.is_enabled("enforce-captcha", default=True)
def should_enforce_captcha(flags) -> bool:
    # Defaults to enforcing if the flag is missing
    return flags.is_enabled("enforce-captcha", default=True)
def should_enforce_captcha(flags) -> bool:
    # Defaults to enforcing if the flag is missing
    return flags.is_enabled("enforce-captcha", default=True)
def should_enforce_captcha(flags) -> bool:
    # Defaults to enforcing if the flag is missing
    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")

    # The browser must be on the domain before a cookie can be set
    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")

    # The browser must be on the domain before a cookie can be set
    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")

    # The browser must be on the domain before a cookie can be set
    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")

    # The browser must be on the domain before a cookie can be set
    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:
    # Never allowlist by email in production: anyone can type any address
    if os.getenv("APP_ENV") == "production":
        return True
    domain = email.rsplit("@", 1)[-1].lower()
    return domain not in TEST_EMAIL_DOMAINS
import os

TEST_EMAIL_DOMAINS = {"test.example.com"}


def captcha_required_for(email: str) -> bool:
    # Never allowlist by email in production: anyone can type any address
    if os.getenv("APP_ENV") == "production":
        return True
    domain = email.rsplit("@", 1)[-1].lower()
    return domain not in TEST_EMAIL_DOMAINS
import os

TEST_EMAIL_DOMAINS = {"test.example.com"}


def captcha_required_for(email: str) -> bool:
    # Never allowlist by email in production: anyone can type any address
    if os.getenv("APP_ENV") == "production":
        return True
    domain = email.rsplit("@", 1)[-1].lower()
    return domain not in TEST_EMAIL_DOMAINS
import os

TEST_EMAIL_DOMAINS = {"test.example.com"}


def captcha_required_for(email: str) -> bool:
    # Never allowlist by email in production: anyone can type any address
    if os.getenv("APP_ENV") == "production":
        return True
    domain = email.rsplit("@", 1)[-1].lower()
    return domain not in TEST_EMAIL_DOMAINS

Test 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

# Replace with your CI runners' static IP range
TEST_RUNNER_NETWORKS = [ipaddress.ip_network("203.0.113.0/28")]


def captcha_required_for_ip(remote_addr: str) -> bool:
    # remote_addr must come from the actual connection or a trusted proxy,
    # never from a header the client can set, like X-Forwarded-For
    ip = ipaddress.ip_address(remote_addr)
    return not any(ip in network for network in TEST_RUNNER_NETWORKS)
import ipaddress

# Replace with your CI runners' static IP range
TEST_RUNNER_NETWORKS = [ipaddress.ip_network("203.0.113.0/28")]


def captcha_required_for_ip(remote_addr: str) -> bool:
    # remote_addr must come from the actual connection or a trusted proxy,
    # never from a header the client can set, like X-Forwarded-For
    ip = ipaddress.ip_address(remote_addr)
    return not any(ip in network for network in TEST_RUNNER_NETWORKS)
import ipaddress

# Replace with your CI runners' static IP range
TEST_RUNNER_NETWORKS = [ipaddress.ip_network("203.0.113.0/28")]


def captcha_required_for_ip(remote_addr: str) -> bool:
    # remote_addr must come from the actual connection or a trusted proxy,
    # never from a header the client can set, like X-Forwarded-For
    ip = ipaddress.ip_address(remote_addr)
    return not any(ip in network for network in TEST_RUNNER_NETWORKS)
import ipaddress

# Replace with your CI runners' static IP range
TEST_RUNNER_NETWORKS = [ipaddress.ip_network("203.0.113.0/28")]


def captcha_required_for_ip(remote_addr: str) -> bool:
    # remote_addr must come from the actual connection or a trusted proxy,
    # never from a header the client can set, like X-Forwarded-For
    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):
    # Runs against the test environment, where captcha verification is bypassed
    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.text
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):
    # Runs against the test environment, where captcha verification is bypassed
    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.text
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):
    # Runs against the test environment, where captcha verification is bypassed
    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.text
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):
    # Runs against the test environment, where captcha verification is bypassed
    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.text

This 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:
    # The mock token is accepted in test environments and rejected everywhere else
    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:
    # The mock token is accepted in test environments and rejected everywhere else
    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:
    # The mock token is accepted in test environments and rejected everywhere else
    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:
    # The mock token is accepted in test environments and rejected everywhere else
    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:

# In the test: submit the mock token in place of a real captcha response
driver.execute_script(
    "document.querySelector('[name=\"g-recaptcha-response\"]').value = arguments[0];",
    "test-captcha-token",
)
# In the test: submit the mock token in place of a real captcha response
driver.execute_script(
    "document.querySelector('[name=\"g-recaptcha-response\"]').value = arguments[0];",
    "test-captcha-token",
)
# In the test: submit the mock token in place of a real captcha response
driver.execute_script(
    "document.querySelector('[name=\"g-recaptcha-response\"]').value = arguments[0];",
    "test-captcha-token",
)
# In the test: submit the mock token in place of a real captcha response
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

# With Google's test keys, ticking the checkbox always passes
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

# With Google's test keys, ticking the checkbox always passes
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

# With Google's test keys, ticking the checkbox always passes
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

# With Google's test keys, ticking the checkbox always passes
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

  1. 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")
        # ...create the account
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")
        # ...create the account
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")
        # ...create the account
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")
        # ...create the account

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")
    # ...assert the account was created
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")
    # ...assert the account was created
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")
    # ...assert the account was created
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")
    # ...assert the account was created
  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

  1. 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

Frequently Asked Questions

Can I use captcha solving services for testing?

Will Google detect and block my test automation?

How do I test that captcha actually works?

What about invisible captchas like reCAPTCHA v3?

Should I remove captcha from login for all test accounts?

How do other organizations handle captcha in automation?

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

See what your next release looks like with Virtuoso

Book a walkthrough on your applications and your workflows. Bring a requirement, a user journey, or a brittle Selenium script, and watch the loop run on something you recognise.

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA

Virtuoso QA is establishing the standard of proof for software releases. Its governed QA loop turns business requirements into tests for any browser-based application: AI proposes, a deterministic engine executes, a person approves what matters, and every decision leaves evidence.

Trust Center

AICPA

SOC

WAVE STRONG PERFORMER

@ Copyright 2026 SpotQA, Creators of Virtuoso QA