Tích Hợp

Cypress + CaptchaAI: Thử nghiệm E2E bằng cách xử lý CAPTCHA

Cypress không tự giải được CAPTCHA — đó là câu trả lời ngắn cho câu hỏi nhiều đội QA gặp khi bộ test E2E dừng lại ở bước đăng nhập hoặc thanh toán. Tắt CAPTCHA ở staging tạo ra môi trường test khác hẳn production: lỗi đặt token vào biểu mẫu QA, lỗi callback chỉ lộ ra khi CAPTCHA thật đang chạy. Cách xử lý đúng: để Cypress gọi CaptchaAI qua một task handler trong cypress.config.js, nhận token thật rồi tiêm ngược vào form.


Bốn bước CaptchaAI xử lý CAPTCHA trong một lần test

Nắm luồng xử lý chung trước khi vào code. Mọi tích hợp CaptchaAI — kể cả trong Cypress — đều đi qua đúng bốn bước:

  1. Cypress gửi sitekey + URL trang tới in.php, nhận về một task ID.
  2. Task handler polling res.php mỗi 5 giây bằng task ID đó.
  3. CaptchaAI trả token thật khi giải xong (hoặc CAPCHA_NOT_READY nếu chưa xong).
  4. Custom command đặt token vào biểu mẫu QA vào field ẩn của form (g-recaptcha-response hoặc cf-turnstile-response) rồi kích hoạt callback nếu site có.

Vì sao không nên tắt CAPTCHA khi chạy test E2E?

Ba cách tiếp cận phổ biến, và chỉ một cách giữ được độ tin cậy tương đương production:

Cách tiếp cận Rủi ro
Tắt CAPTCHA ở môi trường staging Bỏ sót lỗi tích hợp, luồng biểu mẫu chạy khác production
Dùng site key kiểm thử (luôn qua) Không kiểm tra việc đặt token vào biểu mẫu QA, không kiểm tra xử lý callback
Giải bằng CaptchaAI Test đúng như production, đầy đủ CAPTCHA thật

Dấu hiệu bộ test đang né CAPTCHA thay vì test CAPTCHA thật:

  • Test pass ở CI nhưng người dùng thật vẫn gặp lỗi ở form có CAPTCHA trên production.
  • Không dòng code nào tiêm token hoặc gọi callback ___grecaptcha_cfg.
  • Biến DISABLE_CAPTCHA=true chỉ tồn tại ở staging, không có trên production.

Nhiều đội QA outsourcing ở TP.HCM và Hà Nội viết test Cypress cho ứng dụng thương mại điện tử kiểu Shopee/Tiki, nơi checkout được bảo vệ bằng Cloudflare Turnstile — bỏ qua bước giải CAPTCHA nghĩa là lỗi đặt token vào biểu mẫu QA chỉ lộ ra khi khách đang thanh toán thật.


Khi nào KHÔNG cần CaptchaAI trong bộ test Cypress

Không phải test nào cũng cần CAPTCHA thật:

  • Component testing không tải trang thật, cy.solveCaptcha() không có data-sitekey để đọc — dùng mock token.
  • Unit test xác thực logic form (validate email, độ dài mật khẩu…) không liên quan CAPTCHA.
  • Chỉ dùng CaptchaAI cho E2E chạy trên URL trang thật, nơi CAPTCHA thật đang hiển thị.

Thiết lập dự án Cypress

Cài Cypress bình thường, không cần plugin riêng cho CaptchaAI:

npm install cypress --save-dev

Cấu hình Cypress (cypress.config.js)

Khai báo task tên solveCaptcha trong setupNodeEvents — Node.js (không phải trình duyệt) gọi API CaptchaAI để tránh giới hạn CORS:

// 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",
  },
});

Viết task handler gọi API CaptchaAI

Handler này gửi CAPTCHA tới in.php, polling res.php cho tới khi có token — đúng bốn bước ở trên:

// 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 };

Kết nối handler với cypress.config.js

Import handler ở trên và đăng ký lại task, thay thế bản khai báo rút gọn ban đầu bằng bản trỏ thẳng tới captcha-solver.js:

// 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 command: solveCaptchasolveTurnstile

Gói task thành hai custom command gọi được như lệnh Cypress bình thường. solveCaptcha tự đọc data-sitekey, gọi task, đặt token vào biểu mẫu QA vào #g-recaptcha-response và callback ___grecaptcha_cfg nếu có:

// 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;
        });
      });
    });
  });
});

Ví dụ test E2E thực tế với CaptchaAI

Test luồng đăng nhập có 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");
  });
});

Test luồng đăng ký tài khoản

// 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("[email protected]");
    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");
  });
});

Test checkout có Cloudflare Turnstile

Cùng pattern, chỉ đổi sang solveTurnstile() vì Turnstile dùng field token khác (cf-turnstile-response):

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");
  });
});

Retry và xử lý lỗi khi giải CAPTCHA thất bại

CAPTCHA thật thỉnh thoảng timeout hoặc trả token rỗng — thêm retry với backoff ngắn thay vì để pipeline fail ngay:

// 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);
});

Chạy Cypress + CaptchaAI trong pipeline CI/CD

Mẹo: tách test có CAPTCHA vào job riêng, không chạy chung unit test — mỗi lần giải cộng thêm 15-30 giây.

GitHub Actions

Đặt CAPTCHAAI_KEY vào secrets của repo, không hardcode trong file cấu hình:

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

Test tích hợp với Jest (tầng API)

Đội cũng dùng Jest để test CAPTCHA tầng API thì tái sử dụng thẳng handler đã viết ở trên:

// 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);

Câu hỏi thường gặp

Cypress có tự giải được CAPTCHA mà không cần dịch vụ ngoài không?

Không. Cypress chỉ điều khiển trình duyệt và assertion, không có engine giải CAPTCHA. Cần một service như CaptchaAI nhận sitekey, trả token, rồi tiêm vào form qua custom command như ở trên.

CaptchaAI có hỗ trợ hCaptcha trong bộ test Cypress không?

Chưa. CaptchaAI hiện giải reCAPTCHA v2/v3, Cloudflare Turnstile và Challenge, GeeTest v3, CAPTCHA ảnh/OCR, grid image và BLS; hCaptcha, FunCaptcha chưa hỗ trợ. Custom command ở bài này chưa dùng được cho hCaptcha.

Nên test với CAPTCHA thật hay mock trong pipeline CI?

Dùng CAPTCHA thật ở tầng E2E, mock ở unit test. Mock nhanh, ổn định cho logic component, nhưng chỉ CAPTCHA thật mới bắt được lỗi đặt token vào biểu mẫu QA và lỗi callback — đúng lỗi chỉ lộ ra trên production.

Chạy song song trên Cypress Cloud có cần nhiều API key CaptchaAI không?

Không cần nhiều key. Mỗi máy song song gọi chung một API key, CaptchaAI xử lý request đồng thời theo số thread của gói bạn dùng — BASIC ($15/tháng, 5 thread) đủ cho vài máy chạy test nhẹ, nhiều container hơn thì cần gói nhiều thread hơn.


Các lỗi thường gặp và cách xử lý

Vấn đề Nguyên nhân Cách xử lý
cy.task timed out Quá trình giải CAPTCHA mất quá lâu Tăng taskTimeout trong cấu hình
Token bị từ chối Token hết hạn trước khi tiêm Giảm độ trễ giữa lúc giải xong và lúc submit
Không tìm thấy data-sitekey CAPTCHA tải động (lazy-load) Thêm cy.wait() rõ ràng hoặc chặn (intercept) request
Callback không được kích hoạt Site dùng tên callback tùy chỉnh Kiểm tra ___grecaptcha_cfg trong DevTools
CI fail, chạy local pass Thiếu biến môi trường Thêm CAPTCHAAI_KEY vào secrets của CI

Đọc thêm và bước tiếp theo

  1. CaptchaAI Quickstart: Lần Giải CAPTCHA Đầu Tiên Của Bạn Trong 5 Phút
  2. Cách Giải reCAPTCHA v2 Bằng API: Hướng Dẫn Từng Bước
  3. Cách giải Cloudflare Turnstile bằng API
  4. Cách giải quyết GeeTest v3 bằng API
  5. Puppeteer + CaptchaAI trong browser automation
  6. API giải CAPTCHA bằng Node.js
Os comentários estão desativados para este artigo.