Why Run Tests in CI?
Running tests locally is a good habit. Running them automatically on every pull request is a team habit — one that prevents broken code from reaching production. GitHub Actions makes this remarkably easy, and it's free for public repositories.
This guide walks you through setting up a basic CI testing workflow for a JavaScript project using GitHub Actions. The core concepts apply to any language or framework.
Prerequisites
- A GitHub repository with a test suite (we'll use Jest as the example)
- A
package.jsonwith a"test"script defined - Basic familiarity with YAML syntax
Step 1: Create the Workflow File
GitHub Actions workflows live in .github/workflows/ at the root of your repository. Create a file called ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Commit this file and push it. GitHub will automatically detect it and start running your workflow.
Step 2: Understand the Key Concepts
- Triggers (
on:) — Defines when the workflow runs. Here it runs on pushes tomainand on any pull request targetingmain. - Jobs — A workflow contains one or more jobs. Jobs run in parallel by default.
- Steps — Each job has ordered steps. Steps can use pre-built actions (
uses:) or run shell commands (run:). npm ci— Use this instead ofnpm installin CI. It's faster and uses your lockfile exactly.
Step 3: Add a Status Badge to Your README
Once your workflow is running, add a badge to your README so everyone on the team can see the build status at a glance:

Step 4: Run Tests Against Multiple Node Versions
If your project needs to support multiple runtime versions, use a matrix strategy:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
This runs three parallel jobs — one for each Node version — and reports failures independently.
Step 5: Fail Fast and Protect Your Main Branch
Go to your repository Settings → Branches → Branch protection rules and add a rule for main. Enable "Require status checks to pass before merging" and select your CI workflow. Now no PR can be merged with a failing test suite — even if someone tries.
Beyond the Basics
Once your basic test pipeline is running, here are natural next steps:
- Upload test results as artifacts for debugging failed runs
- Add code coverage reporting with tools like Codecov or Coveralls
- Integrate end-to-end tests (Playwright or Cypress have official GitHub Actions support)
- Cache dependencies more aggressively to speed up runs
A CI pipeline that takes under 3 minutes to run is one your team will actually trust and rely on. Start simple, then optimize from there.