Skip to main content

Jest Testing in React GitHub Actions

Jest is a testing framework for JavaScript that executes unit and integration tests, measures code coverage, and reports results. Running Jest in GitHub Actions ensures that every commit is tested automatically, preventing regressions from reaching production. CI test runs provide visibility into test health and catch bugs that manual testing or type checking might miss.

Why Test in CI?

Tests are automated assertions that verify code behavior. Running tests in CI ensures every code change is validated against your test suite before merging. Teams that run tests in CI report 30-40% fewer post-release bugs (source: ThoughtWorks Technology Radar 2023). Tests also serve as living documentation: they show how functions are meant to be used and what behavior is expected.

Without CI testing, developers might skip running tests locally, or tests might pass on one environment but fail on another (environment drift). CI testing enforces that all code is tested and that tests pass consistently.

Setting Up Jest for React

Install Jest and testing utilities:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom @babel/preset-react

Create a jest.config.js in your project root:

export default {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/index.js',
'!src/**/*.d.ts',
],
};

Create src/setupTests.js:

import '@testing-library/jest-dom';

Add test scripts to package.json:

{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
}
}

Running Tests in Your Workflow

Add a test job to your workflow:

name: React CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

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'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm run test -- --coverage --watchAll=false

build:
runs-on: ubuntu-latest
needs: test

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm ci

- name: Build React app
run: npm run build

Key flags:

  • --coverage: Generates a coverage report showing which code is tested
  • --watchAll=false: Runs tests once and exits (required in CI, which doesn't support interactive watch mode)

Example: Testing a React Component

Create src/Button.jsx:

import React from 'react';

export default function Button({ label, onClick, disabled }) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
);
}

Create src/Button.test.jsx:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

describe('Button', () => {
it('renders with label', () => {
render(<Button label="Click me" onClick={() => {}} />);
expect(screen.getByText('Click me')).toBeInTheDocument();
});

it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button label="Click me" onClick={handleClick} />);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('disables button when disabled prop is true', () => {
render(<Button label="Click me" onClick={() => {}} disabled />);
expect(screen.getByRole('button')).toBeDisabled();
});
});

Running npm run test in CI executes these tests and reports pass/fail. If any test fails, the build fails.

Coverage Reports and Thresholds

Code coverage measures what percentage of your code is executed by tests. Set coverage thresholds in jest.config.js to enforce minimum coverage:

export default {
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/index.js',
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
};

This enforces that at least 70% of branches, functions, lines, and statements are covered. If coverage falls below the threshold, Jest fails with an error.

Test Types and When to Use Them

Test TypeSpeedCoverageBest For
Unit testsFastSmall unitsFunctions, hooks, utilities
Integration testsMediumComponents + logicComponent behavior, data flow
End-to-end testsSlowFull appUser workflows, critical paths

In CI, run unit and integration tests (Jest). Save E2E tests for a separate, slower job or run them on main only.

Key Takeaways

  • Running tests in CI ensures code quality and catches regressions automatically
  • Use npm run test -- --coverage --watchAll=false to run tests once with coverage reports
  • Coverage thresholds enforce minimum testing standards and prevent untested code from merging
  • Test different component behaviors: rendering, user interactions, prop variations, edge cases
  • Jest + Testing Library are the standard for React testing in 2026

Frequently Asked Questions

Why does my test pass locally but fail in CI?

Common causes: missing environment variables, file path differences between Windows/Linux, timing issues (async code). Run tests locally with npm test -- --watchAll=false to match CI behavior exactly.

Should I test third-party components?

No. Mock third-party libraries and focus on testing your own code. Jest provides jest.mock() for this:

jest.mock('react-router-dom', () => ({
useNavigate: jest.fn(),
}));

How do I improve test coverage?

Use the coverage report: npm run test -- --coverage generates an HTML report (coverage/lcov-report/index.html) showing which lines are untested. Target high-coverage areas first.

What's the difference between fireEvent and userEvent?

fireEvent simulates raw DOM events; userEvent simulates realistic user interactions (e.g., typing, clicking with modifier keys). Prefer userEvent for more realistic tests.

Further Reading