Integrationen

Cypress + CaptchaAI: E2E-Tests mit CAPTCHA-Handhabung

Cypress kann Formulare hinter reCAPTCHA v2 oder Cloudflare Turnstile durchtesten, ohne dass Sie den Schutz im Staging abschalten müssen. CaptchaAI übernimmt die CAPTCHA-Abfrage über einen cy.task, gibt ein gültiges Token zurück, und Ihr Test füllt das Formular aus und sendet es ab – wie ein echter Nutzer. So bleibt Ihre Testumgebung deckungsgleich mit der Produktion, statt an einer abgeschalteten Sicherheitsschicht vorbeizulaufen.

Genau das ist der Kern: Ein E2E-Test, der das CAPTCHA aussperrt, testet einen Ablauf, den es in der Produktion so nicht gibt. Fehler im Token-Handling, im Callback oder im Formularfluss fallen dann erst live auf.


Voraussetzungen

Bevor der erste Test läuft, brauchen Sie drei Dinge:

  • einen CaptchaAI-API-Schlüssel, über den Schnellstart in wenigen Minuten erzeugt;
  • eine aktuelle Node.js-Umgebung;
  • Cypress als Dev-Abhängigkeit im Projekt.

Der Schlüssel wird nie im Testcode hinterlegt, sondern über eine Umgebungsvariable eingelesen – lokal wie in der CI.

Der grundsätzliche Ablauf ist in allen Beispielen dieses Leitfadens derselbe: Cypress liest den sitekey aus der Seite, ein Node-Task reicht die Aufgabe an CaptchaAI weiter, das zurückgegebene Token landet im versteckten Response-Feld, und erst danach klickt der Test auf „Absenden". Kein Schritt umgeht dabei den Schutz – er wird regulär gelöst, wie es auch ein Nutzer täte.


Abschalten, Test-Key oder echt lösen?

Für den CAPTCHA-Schritt im Test gibt es drei Wege – nur einer prüft den echten Produktionspfad:

Ansatz Risiko
CAPTCHA im Staging deaktivieren Übersieht Integrationsfehler und Abweichungen im Formularfluss
Test-Keys (immer bestehen) verwenden Prüft weder das Einfügen des Tokens noch die Callback-Verarbeitung
Mit CaptchaAI lösen Volle Produktionsparität im Test

Installation und Konfiguration

Installieren Sie Cypress als Dev-Abhängigkeit:

npm install cypress --save-dev

Cypress konfigurieren

Registrieren Sie einen Node-Task für das Lösen und heben Sie die Timeouts an – ein CAPTCHA-Solve dauert länger als ein gewöhnlicher Cypress-Befehl:

// cypress.config.js
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    baseUrl: "https://your-app.com",
    defaultCommandTimeout: 120000,
    responseTimeout: 120000,
    setupNodeEvents(on, config) {
      on("task", {
        solveCaptcha({ siteUrl, sitekey, type }) {
          return solveCaptchaTask(siteUrl, sitekey, type);
        },
      });
      return config;
    },
  },
  env: {
    CAPTCHAAI_KEY: "YOUR_API_KEY",
  },
});

Der Solver als Node-Task

Der Task läuft im Node-Prozess von Cypress, nicht im Browser. Er übermittelt die Seite an in.php, fragt das Ergebnis über res.php im Polling ab und gibt am Ende das Token zurück. Für reCAPTCHA v2 wird googlekey gesetzt, für Turnstile sitekey – die Methode entscheidet über das Feld:

// cypress/plugins/captcha-solver.js
const https = require("https");

function httpPost(url, data) {
  return new Promise((resolve, reject) => {
    const params = new URLSearchParams(data).toString();
    const options = {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
    };
    const req = https.request(url, options, (res) => {
      let body = "";
      res.on("data", (c) => (body += c));
      res.on("end", () => resolve(JSON.parse(body)));
    });
    req.on("error", reject);
    req.write(params);
    req.end();
  });
}

function httpGet(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let body = "";
      res.on("data", (c) => (body += c));
      res.on("end", () => resolve(JSON.parse(body)));
    }).on("error", reject);
  });
}

async function solveCaptchaTask(siteUrl, sitekey, type = "recaptcha_v2") {
  const API = "https://ocr.captchaai.com";
  const key = process.env.CAPTCHAAI_KEY || "YOUR_API_KEY";

  const submitData = {
    key,
    pageurl: siteUrl,
    json: "1",
  };

  if (type === "turnstile") {
    submitData.method = "turnstile";
    submitData.sitekey = sitekey;
  } else {
    submitData.method = "userrecaptcha";
    submitData.googlekey = sitekey;
  }

  const submitResp = await httpPost(`${API}/in.php`, submitData);

  if (submitResp.status !== 1) {
    throw new Error(`Submit failed: ${submitResp.request}`);
  }

  const taskId = submitResp.request;

  // Poll for result
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));

    const params = new URLSearchParams({
      key,
      action: "get",
      id: taskId,
      json: "1",
    });

    const result = await httpGet(`${API}/res.php?${params}`);

    if (result.request === "CAPCHA_NOT_READY") continue;
    if (result.status !== 1) throw new Error(`Solve failed: ${result.request}`);

    return result.request; // The CAPTCHA token
  }

  throw new Error("CAPTCHA solve timeout");
}

module.exports = { solveCaptchaTask };

In cypress.config.js einbinden

// cypress.config.js
const { solveCaptchaTask } = require("./cypress/plugins/captcha-solver");

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on("task", {
        solveCaptcha({ siteUrl, sitekey, type }) {
          return solveCaptchaTask(siteUrl, sitekey, type);
        },
      });
    },
  },
});

Custom Commands für reCAPTCHA und Turnstile

Damit die Tests lesbar bleiben, kapseln Sie das Lösen in eigene Cypress-Befehle. solveCaptcha liest den data-sitekey aus der Seite, ruft den Task auf, trägt das Token in die versteckten Response-Felder ein und löst – falls vorhanden – den reCAPTCHA-Callback aus. solveTurnstile macht dasselbe für das cf-turnstile-response-Feld:

// cypress/support/commands.js

Cypress.Commands.add("solveCaptcha", (options = {}) => {
  cy.get("[data-sitekey]", { timeout: 10000 }).then(($el) => {
    const sitekey = options.sitekey || $el.attr("data-sitekey");
    const siteUrl = options.siteUrl || cy.url();

    cy.url().then((url) => {
      cy.task("solveCaptcha", {
        siteUrl: url,
        sitekey,
        type: options.type || "recaptcha_v2",
      }).then((token) => {
        // Inject token
        cy.window().then((win) => {
          const responseEl = win.document.querySelector(
            "#g-recaptcha-response"
          );
          if (responseEl) {
            responseEl.value = token;
          }

          // Set all hidden response fields
          win.document
            .querySelectorAll('[name="g-recaptcha-response"]')
            .forEach((el) => {
              el.value = token;
            });

          // Trigger callback if exists
          if (win.___grecaptcha_cfg) {
            const clients = win.___grecaptcha_cfg.clients;
            for (const key in clients) {
              const client = clients[key];
              if (client && typeof client.callback === "function") {
                client.callback(token);
              }
            }
          }
        });
      });
    });
  });
});

Cypress.Commands.add("solveTurnstile", (options = {}) => {
  cy.get("[data-sitekey]", { timeout: 10000 }).then(($el) => {
    const sitekey = options.sitekey || $el.attr("data-sitekey");

    cy.url().then((url) => {
      cy.task("solveCaptcha", {
        siteUrl: url,
        sitekey,
        type: "turnstile",
      }).then((token) => {
        cy.window().then((win) => {
          const input = win.document.querySelector(
            'input[name="cf-turnstile-response"]'
          );
          if (input) input.value = token;
        });
      });
    });
  });
});

E2E-Tests: Login, Registrierung, Checkout

Mit den Custom Commands wird der CAPTCHA-Schritt zu einer einzigen Zeile im Testfall. Setzen Sie durchgängig synthetische Daten ein (wie hier test@example.com) – echte personenbezogene Daten gehören aus DSGVO-Sicht nicht in Testläufe.

Login hinter reCAPTCHA v2

// cypress/e2e/login.cy.js
describe("Login with reCAPTCHA", () => {
  it("should log in through a CAPTCHA-protected form", () => {
    cy.visit("/login");

    cy.get("#username").type("testuser");
    cy.get("#password").type("securepassword123");

    // Solve the CAPTCHA
    cy.solveCaptcha();

    // Submit
    cy.get('button[type="submit"]').click();

    // Verify login success
    cy.url().should("include", "/dashboard");
    cy.get(".welcome-message").should("contain", "Welcome, testuser");
  });
});

Registrierungs-Flow

// cypress/e2e/register.cy.js
describe("Registration with CAPTCHA", () => {
  it("completes registration with all fields + CAPTCHA", () => {
    cy.visit("/register");

    cy.get("#first-name").type("Test");
    cy.get("#last-name").type("User");
    cy.get("#email").type("test@example.com");
    cy.get("#password").type("StrongPass!123");
    cy.get("#confirm-password").type("StrongPass!123");

    cy.solveCaptcha();

    cy.get("#register-btn").click();
    cy.url().should("include", "/verify-email");
  });
});

Checkout mit Turnstile

Der Checkout nutzt eine Testkarte (4242…); das solveTurnstile-Command liefert das Token, bevor „Jetzt bezahlen" geklickt wird:

describe("Checkout with Turnstile", () => {
  it("processes payment through Turnstile-protected checkout", () => {
    cy.visit("/cart");

    cy.get(".checkout-btn").click();
    cy.get("#card-number").type("4242424242424242");
    cy.get("#expiry").type("12/26");
    cy.get("#cvc").type("123");

    cy.solveTurnstile();

    cy.get("#pay-now").click();
    cy.get(".confirmation").should("contain", "Order confirmed");
  });
});

Wiederholungslogik und Fehlerbehandlung

Kommt aus einem Solve kein Token zurück, sollte der Test nicht sofort scheitern. Ein kurzer erneuter Versuch mit Zähler fängt sporadische Aussetzer ab, ohne die Suite endlos zu blockieren:

// cypress/support/commands.js

Cypress.Commands.add("solveCaptchaWithRetry", (options = {}) => {
  const maxRetries = options.retries || 3;

  function attempt(retryCount) {
    return cy.task("solveCaptcha", {
      siteUrl: options.siteUrl,
      sitekey: options.sitekey,
      type: options.type || "recaptcha_v2",
    }).then((token) => {
      if (!token && retryCount < maxRetries) {
        cy.log(`CAPTCHA retry ${retryCount + 1}/${maxRetries}`);
        cy.wait(2000);
        return attempt(retryCount + 1);
      }
      return token;
    });
  }

  return attempt(0);
});

In die CI/CD-Pipeline einbinden

Der API-Schlüssel gehört als Secret in die Pipeline, nie in den Code. In vielen DACH-Teams läuft die Pipeline auf GitLab CI statt auf GitHub Actions – das Muster ist identisch: Hinterlegen Sie CAPTCHAAI_KEY als maskierte CI/CD-Variable und rufen Sie denselben Testbefehl auf.

GitHub Actions

name: E2E Tests
on: [push, pull_request]

jobs:
  cypress:
    runs-on: ubuntu-latest
    steps:

      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci

      - name: Run Cypress tests
        uses: cypress-io/github-action@v6
        env:
          CAPTCHAAI_KEY: ${{ secrets.CAPTCHAAI_KEY }}
        with:
          wait-on: "http://localhost:3000"
          start: npm start

Jest auf API-Ebene

Wer den Solve zusätzlich losgelöst von der UI prüfen will, ruft den Task-Handler direkt aus einem Jest-Test auf – nützlich als schneller Smoke-Test der CaptchaAI-Anbindung:

// For teams that also use Jest for API-level CAPTCHA tests
const { solveCaptchaTask } = require("../cypress/plugins/captcha-solver");

test("CaptchaAI solves reCAPTCHA v2", async () => {
  const token = await solveCaptchaTask(
    "https://www.google.com/recaptcha/api2/demo",
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    "recaptcha_v2"
  );

  expect(token).toBeDefined();
  expect(token.length).toBeGreaterThan(50);
}, 120000);

Typische Fehler beheben

Problem Ursache Lösung
cy.task timed out Das Lösen des CAPTCHA hat zu lange gedauert taskTimeout in der Konfiguration erhöhen
Token abgelehnt Vor dem Einfügen abgelaufen Verzögerung zwischen Lösung und Übermittlung verkürzen
data-sitekey nicht gefunden CAPTCHA wird dynamisch geladen Explizites cy.wait() ergänzen oder Request abfangen
Callback nicht ausgelöst Benutzerdefinierter Callback-Name ___grecaptcha_cfg in den DevTools prüfen
CI schlägt fehl, lokal läuft es Fehlende Umgebungsvariable CAPTCHAAI_KEY zu den CI-Secrets hinzufügen

Häufige Fragen

Was kostet das Lösen pro Testlauf?

CaptchaAI rechnet Thread-basiert ab, nicht pro Lösung – jeder Thread löst unbegrenzt viele CAPTCHAs im Abrechnungsmonat. BASIC startet bei 15 $/Monat mit 5 Threads; für parallele CI-Läufe skalieren Sie über die Thread-Zahl, nicht über einen Preis pro Solve. Die aktuellen Tarife stehen auf der Preisseite von CaptchaAI.

Welche CAPTCHA-Typen deckt dieser Ansatz ab?

Der solveCaptcha-Task funktioniert für reCAPTCHA v2 (inkl. v3 über dieselbe Methode) und Cloudflare Turnstile – die beiden in E2E-Formularen mit Abstand häufigsten Typen. hCaptcha wird von CaptchaAI nicht unterstützt; für Bild- oder Grid-CAPTCHAs setzen Sie den passenden Methoden-Parameter.

Verlangsamt das meine Testsuite?

Ja, jeder Solve kostet real Zeit – rechnen Sie mit 15–30 Sekunden pro CAPTCHA. Lagern Sie CAPTCHA-Tests in eine eigene Suite aus oder parallelisieren Sie über Cypress Cloud, damit die schnelle Feedback-Schleife der übrigen Tests erhalten bleibt.

Funktioniert das in GitLab CI genauso wie in GitHub Actions?

Ja. Der Code ist CI-unabhängig; Sie brauchen nur CAPTCHAAI_KEY als maskierte Variable und einen Runner, der Cypress ausführt. Bei paralleler Ausführung ruft jede Maschine denselben API-Schlüssel auf – CaptchaAI verarbeitet gleichzeitige Anfragen im Rahmen Ihrer Thread-Zahl.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.