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.json with 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 to main and on any pull request targeting main.
  • 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 of npm install in 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:

![CI](https://github.com/YOUR_USERNAME/YOUR_REPO/actions/workflows/ci.yml/badge.svg)

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.