TestCafe in CI/CD: GitHub Actions & GitLab Pipelines
Getting TestCafe running locally is straightforward. Getting it to run reliably in CI — across PRs, on merge, and on a schedule — requires configuring headless browsers, handling flakiness, managing environment variables, and collecting artifacts when tests fail.
This guide covers production-ready TestCafe CI setups for GitHub Actions and GitLab CI.
Why TestCafe Is CI-Friendly
TestCafe has a genuine advantage in CI: no browser drivers to install. It uses system browsers directly, so a standard node:18 container with Chrome pre-installed is all you need. Compare this to WebDriver-based tools that require exact driver version matching.
Other CI-friendly defaults:
--headlessflag works without display server configuration- Reporters output JUnit XML, JSON, or spec format
--fail-on-first-test-errorstops the run immediately on failures- Exit codes work correctly for CI pass/fail detection
Basic GitHub Actions Setup
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
e2e:
name: TestCafe E2E
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run TestCafe tests
run: npx testcafe chrome:headless tests/ --reporter spec
- name: Upload screenshots on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: testcafe-screenshots
path: screenshots/
retention-days: 7Key points:
npm ciis faster thannpm installin CI — uses lockfile exactlychrome:headlessavoids needing a display server (Xvfb)if: failure()only uploads artifacts when something goes wrong
Adding Test Reports
JUnit XML output integrates with GitHub's test reporter actions and most CI dashboards:
- name: Run TestCafe tests
run: |
npx testcafe chrome:headless tests/ \
--reporter spec,junit:reports/junit.xml \
--screenshots path=reports/screenshots,takeOnFails=true
- name: Publish test report
uses: mikepenz/action-junit-report@v4
if: always()
with:
report_paths: reports/junit.xml
check_name: TestCafe Results
- name: Upload reports
uses: actions/upload-artifact@v4
if: always()
with:
name: test-reports
path: reports/Environment Variables and Secrets
E2E tests usually need credentials. Pass them through GitHub Secrets:
- name: Run TestCafe tests
run: npx testcafe chrome:headless tests/
env:
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
BASE_URL: https://staging.example.com
HELPMETEST_API_TOKEN: ${{ secrets.HELPMETEST_API_TOKEN }}Access in tests:
fixture('User Login')
.page(process.env.BASE_URL || 'http://localhost:3000');
test('Login with test user', async t => {
await t
.typeText('#email', process.env.TEST_USER_EMAIL)
.typeText('#password', process.env.TEST_USER_PASSWORD)
.click('button[type="submit"]')
.expect(Selector('.dashboard').visible).ok();
});Never hardcode credentials in test files. Even if the repo is private, credentials in source control are a security risk.
Parallel Test Execution
Running tests in parallel dramatically cuts CI time for large suites:
- name: Run TestCafe tests in parallel
run: |
npx testcafe chrome:headless tests/ \
--concurrency 4 \
--reporter junit:reports/junit.xmlFor even faster runs, split tests across multiple CI jobs using a matrix:
jobs:
e2e:
strategy:
matrix:
shard: [1, 2, 3, 4]
fail-fast: false
steps:
- name: Run test shard
run: |
npx testcafe chrome:headless tests/ \
--reporter junit:reports/junit-${{ matrix.shard }}.xml \
--test-grep "Shard${{ matrix.shard }}"With test file naming conventions (e.g., shard1-cart.test.js, shard2-checkout.test.js), you can distribute load across jobs.
TestCafe Configuration File
Centralize configuration in .testcaferc.json so your CI command stays clean:
{
"browsers": ["chrome:headless"],
"src": ["tests/**/*.test.js"],
"reporter": [
{ "name": "spec" },
{ "name": "junit", "output": "reports/junit.xml" }
],
"screenshots": {
"path": "reports/screenshots",
"takeOnFails": true,
"fullPage": true
},
"videoPath": "reports/videos",
"assertionTimeout": 5000,
"selectorTimeout": 5000,
"pageLoadTimeout": 30000,
"quarantineMode": {
"successThreshold": 1,
"attemptLimit": 3
}
}quarantineMode retries flaky tests — a test is marked failed only if it fails on every attempt. This reduces false positives from transient network issues.
GitLab CI Setup
# .gitlab-ci.yml
stages:
- test
variables:
NODE_VERSION: "18"
e2e-tests:
stage: test
image: node:${NODE_VERSION}
before_script:
# Install Chrome
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
- echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list
- apt-get update && apt-get install -y google-chrome-stable
- npm ci
script:
- npx testcafe chrome:headless tests/ --reporter spec,junit:reports/junit.xml
after_script:
- echo "Tests completed"
artifacts:
reports:
junit: reports/junit.xml
paths:
- reports/screenshots/
- reports/videos/
when: always
expire_in: 1 week
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHUsing a pre-built Chrome image (faster — no Chrome install step):
e2e-tests:
image: zenika/alpine-chrome:with-node
before_script:
- npm ci
script:
- npx testcafe chrome:headless --no-sandbox tests/The --no-sandbox flag is required when running Chrome as root in Docker containers.
Multi-Environment Testing
Test against staging before production promotion:
# GitHub Actions — test staging on PR, production on merge
jobs:
e2e-staging:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx testcafe chrome:headless tests/
env:
BASE_URL: https://staging.example.com
e2e-production-smoke:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx testcafe chrome:headless tests/smoke/
env:
BASE_URL: https://example.comThis pattern:
- Runs the full test suite against staging on every PR
- Runs smoke tests against production after every merge to main
Handling Flaky Tests
The most common CI TestCafe problems and fixes:
Tests pass locally, fail in CI:
{
"assertionTimeout": 8000,
"selectorTimeout": 8000,
"pageLoadTimeout": 60000
}CI machines are often slower than local. Increase timeouts in your CI config.
Network-dependent tests fail intermittently:
{
"quarantineMode": {
"successThreshold": 1,
"attemptLimit": 3
}
}Quarantine mode retries tests up to 3 times before marking them failed.
Chrome crashes in Docker:
npx testcafe chrome:headless --no-sandbox --disable-dev-shm-usage tests/Docker containers limit /dev/shm (shared memory). --disable-dev-shm-usage moves Chrome's memory to /tmp.
Tests fail on specific elements:
Enable video recording in CI to see what happened:
{
"videoPath": "reports/videos",
"videoOptions": {
"singleFile": false,
"failedOnly": true
}
}Upload videos as artifacts:
- uses: actions/upload-artifact@v4
if: failure()
with:
name: test-videos
path: reports/videos/Scheduled Production Monitoring
Run TestCafe smoke tests against production on a schedule:
# GitHub Actions — scheduled production smoke tests
on:
schedule:
- cron: '*/30 * * * *' # every 30 minutes
jobs:
production-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx testcafe chrome:headless tests/smoke/
env:
BASE_URL: https://example.com
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: '{"text": "Production smoke tests failed!"}'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}For more robust production monitoring, HelpMeTest runs tests continuously at 5-minute intervals, with built-in alerting, without consuming GitHub Actions minutes or maintaining scheduled workflows:
curl -fsSL https://helpmetest.com/install | bash
helpmetest loginRunning helpmetest health "production" "30m" after each deployment notifies your team if the deployment goes silent — a dead man's switch for your production health.
Complete Production-Ready Workflow
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e:
name: TestCafe E2E (${{ matrix.browser }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
browser: [chrome, firefox]
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- name: Run tests
run: |
npx testcafe ${{ matrix.browser }}:headless tests/ \
--reporter spec,junit:reports/junit-${{ matrix.browser }}.xml \
--screenshots path=reports/screenshots,takeOnFails=true
env:
BASE_URL: ${{ github.event_name == 'push' && 'https://staging.example.com' || 'http://localhost:3000' }}
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: reports-${{ matrix.browser }}
path: reports/
retention-days: 14
- name: Report to HelpMeTest
if: success() && github.ref == 'refs/heads/main'
run: helpmetest health "e2e-tests" "1h"
env:
HELPMETEST_API_TOKEN: ${{ secrets.HELPMETEST_API_TOKEN }}This workflow:
- Cancels in-progress runs on new push (saves CI minutes)
- Tests on Chrome and Firefox in parallel
- Uploads artifacts always (even on success, for historical comparison)
- Reports successful test runs to HelpMeTest health monitoring
Conclusion
TestCafe's CI integration is genuinely low-friction. No driver management, sensible defaults, and a clean exit code model make it reliable in automated pipelines.
The patterns that matter most in production CI: quarantine mode for flakiness, environment variables for credentials, artifact upload on failure, and increasing timeouts for slower CI machines. Pair the CI suite with continuous production monitoring to catch the incidents your tests didn't anticipate.