DevOps · 35 Days · Week 3 Day 13 — Testing in CI
1 / 22
Week 3 · Day 13

Testing in CI

Unit tests, integration tests, code coverage, quality gates — write real tests for your Node.js app, add a coverage threshold, and gate your CI pipeline on quality. Broken code never reaches production.

⏱ Duration 60 min
📖 Theory 25 min
🔧 Lab 30 min
❓ Quiz 5 min
Why testing in CI matters
A pipeline without tests is just an automated deploy button. Tests turn it into a quality gate. Every broken commit caught by CI is a bug that never hit production.
Session Overview

What we cover today

01
The Testing Pyramid
Unit → Integration → E2E. Where to invest. 70/20/10 split. Why most teams over-invest in E2E.
02
Jest — the test framework
describe, test, expect. Common matchers. Mocking. test.each for data-driven tests.
03
Code Coverage
Line, branch, function coverage. Istanbul/v8 under the hood. Reading the coverage report.
04
Coverage Thresholds & Quality Gates
jest.config.js coverageThreshold. Pipeline fails if coverage drops below 70%. SonarQube concept.
05
Test Reports in CI
JUnit XML output, GitHub Actions test summary, Jenkins test trend graphs.
06
Jenkins Testing Integration
junit plugin, publishHTML coverage report, post { always { junit } }.
07
🔧 Lab — Tests + Coverage Gate
Install Jest → write 4 tests → coverage report → 70% threshold → CI gate passes and fails.
Part 1 of 4

The Testing Pyramid — where to invest

E2E Tests Selenium, Playwright, Cypress ~10% · Slowest · Fewest Integration Tests Supertest, DB tests, API tests ~20% · Medium speed Unit Tests Jest, Mocha, Vitest, JUnit, pytest ~70% · Fastest · Most tests
Unit Tests — the foundation
  • Test a single function/component in isolation
  • No database, no network, no filesystem
  • Fake external dependencies with mocks
  • Run in milliseconds — run on every commit
  • If a unit test breaks, you know exactly which function
Integration Tests
  • Test multiple components working together
  • Real database, real HTTP calls (or test doubles)
  • Examples: "POST /login returns 200 and a JWT token"
  • Seconds to minutes to run
E2E Tests — use sparingly
  • Test full user flows in a real browser
  • Most realistic but slowest and most flaky
  • Save for critical paths: signup, checkout, login
  • Run nightly, not on every PR
💡 The anti-pattern: inverted pyramid
Many teams do the opposite — few unit tests, many E2E tests. Result: slow CI (30+ min), flaky failures, hard to debug. Fix: replace 10 E2E tests with 50 unit tests. Same coverage, 10× faster.
Part 2 of 4

Jest — test structure and matchers

Jest test anatomy
// src/utils.js — function under test
function add(a, b) { return a + b; }
function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}
module.exports = { add, divide };

// tests/utils.test.js — test file
const { add, divide } = require('../src/utils');

// describe() groups related tests
describe('add function', () => {
  test('adds two positive numbers', () => {
    expect(add(2, 3)).toBe(5);           // exact equality
  });
  test('handles negative numbers', () => {
    expect(add(-1, 1)).toBe(0);
  });
});

describe('divide function', () => {
  test('divides correctly', () => {
    expect(divide(10, 2)).toBe(5);
  });

  test('throws on division by zero', () => {
    expect(() => divide(5, 0)).toThrow('Division by zero');
  });
});

// === Data-driven tests with test.each ===
test.each([
  [2, 3, 5],
  [0, 0, 0],
  [-1, 1, 0],
])('add(%i, %i) = %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});
Common Jest matchers
.toBe(val)Exact equality (===)
.toEqual(obj)Deep equality (for objects)
.toBeTruthy()Value is truthy
.toContain(item)Array/string contains
.toThrow('msg')Function throws error
.toBeNull()Value is null
.toBeGreaterThan(n)> n
.toHaveBeenCalled()Mock was called
.not.toBe(val)Negate any matcher
Run Jest commands
npx jest                    # Run all tests
npx jest --watch            # Watch mode (re-run on save)
npx jest --coverage         # With coverage report
npx jest utils.test.js      # Run specific file
npx jest --verbose          # Detailed output
npx jest -t "add function"  # Run tests matching name
Part 2 of 4 — continued

Jest Mocking — isolate the unit under test

Mocking in Jest
// === Mock a module ===
jest.mock('../src/db');           // Replace entire module
const db = require('../src/db');

// === Mock a function's return value ===
db.getUser = jest.fn().mockResolvedValue({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com'
});

test('getUserById returns correct user', async () => {
  const user = await db.getUser(1);
  expect(user.name).toBe('Alice');
  expect(db.getUser).toHaveBeenCalledWith(1);   // verify call
  expect(db.getUser).toHaveBeenCalledTimes(1);
});

// === Mock a module with factory ===
jest.mock('axios', () => ({
  get: jest.fn().mockResolvedValue({
    data: { userId: 1, title: 'Test post' }
  })
}));

// === Spy on an existing method ===
const consoleSpy = jest.spyOn(console, 'log')
  .mockImplementation(() => {});   // silence console

// === Reset between tests ===
beforeEach(() => jest.clearAllMocks());
afterEach(() => jest.restoreAllMocks());
Why mocking matters for CI speed
Without mocks: test hits a real database → needs DB running in CI → slow, complex setup.

With mocks: test uses a fake DB that returns instantly → runs in <1ms → zero infra needed.

Unit tests should never need a running service. If they do, they're integration tests.
3 types of test doubles
  • Mock — fake object that records calls. Verify it was called with the right args.
  • Stub — fake that returns a preset value. Control what the dependency returns.
  • Spy — wraps real implementation but records calls. Best of both worlds.
💡 beforeEach / afterEach
beforeEach(() => {}) — runs before each test. Use to reset state.
afterAll(() => {}) — runs once after all tests. Use to close DB connections.
Part 3 of 4

Code Coverage — measuring test completeness

jest.config.js — with thresholds
module.exports = {
  // What to collect coverage from
  collectCoverageFrom: [
    'src/**/*.js',             // all .js in src/
    '!src/**/*.test.js',        // exclude test files
    '!src/index.js',           // exclude entry point
  ],

  // Coverage provider (v8 is faster than babel)
  coverageProvider: 'v8',

  // Output formats
  coverageReporters: [
    'text',        // console table
    'lcov',        // HTML report (coverage/lcov-report/)
    'junit',       // for Jenkins test trending
    'cobertura',   // for GitHub Actions
  ],

  // QUALITY GATE: fail if below these thresholds
  coverageThreshold: {
    global: {
      lines:      70,   // at least 70% lines tested
      branches:   70,   // at least 70% if/else branches
      functions:  70,   // at least 70% functions called
      statements: 70,   // at least 70% statements executed
    },
    // Per-file thresholds (stricter for critical files)
    './src/auth.js': {
      lines: 90,        // auth must be 90%+ tested
    },
  },
};
4 coverage metrics explained
  • Line coverage — what % of lines were executed. Most commonly reported.
  • Branch coverage — what % of if/else paths were taken. More meaningful than line coverage.
  • Function coverage — what % of functions were called at all.
  • Statement coverage — like line coverage but counts each statement (multiple per line possible).

Branch coverage is the most valuable — 100% line coverage with 50% branch coverage means untested else branches.

⚠ Coverage ≠ Quality
100% coverage doesn't mean bug-free. A test that calls a function without asserting its output counts as coverage but tests nothing. Coverage measures what code was run, not whether it was tested correctly. Write assertions that would catch real bugs.
💡 Recommended thresholds
Starting: 50–60% (better than nothing). Healthy: 70–80%. No need to chase 100% — configuration files, generated code, and error paths are hard to test and low risk. Focus coverage effort on business logic.
Part 3 of 4 — continued

Reading coverage reports & quality gates failing

Jest coverage output
$ npx jest --coverage

PASS  tests/utils.test.js
  add function
    ✓ adds two positive numbers (3ms)
    ✓ handles negative numbers (1ms)
  divide function
    ✓ divides correctly (1ms)
    ✓ throws on division by zero (2ms)

---------|---------|----------|---------|---------|
File     | % Stmts | % Branch | % Funcs | % Lines |
---------|---------|----------|---------|---------|
utils.js |   100   |   100   |   100  |   100  |
auth.js  |    45   |    30   |    50  |    45  |  ← UNTESTED
---------|---------|----------|---------|---------|
All files|    62   |    58   |    71  |    62  |

Jest: "global" coverage threshold for lines (70) not met: 62%

# Pipeline exits with code 1 → CI build FAILS ✓
# This is the quality gate working correctly!

# To see which lines are not covered:
# open coverage/lcov-report/index.html in browser
# Red lines = not covered by any test
# Green lines = covered
This is the quality gate in action
When coverage drops below the threshold, Jest exits with code 1. The CI step fails. The pipeline stops. The PR cannot be merged until tests are added.

This is exactly what you want — a developer can't accidentally reduce test coverage without the team noticing.
Where to find the HTML report

After npx jest --coverage:

  • coverage/lcov-report/index.html — open in browser
  • Click any file to see line-by-line coverage
  • Red = not covered. Yellow = partial (branch). Green = fully covered

In CI: upload the coverage directory as a GitHub Actions artifact or publish to Codecov/Coveralls.

💡 Coverage badge
Add a coverage badge to your README:
![Coverage](https://img.shields.io/badge/coverage-78%25-green)
Or connect to Codecov — it auto-posts coverage % to every PR.
Part 4 of 4

Tests in CI — GitHub Actions & Jenkins

GitHub Actions — test + coverage gate
name: CI with Tests

on:
  push:
    branches: [ main, 'feat/**' ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci

      - name: Run tests with coverage
        run: npm test -- --coverage
        # Exits 1 if coverage threshold not met ← gate

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        if: always()        # upload even if tests fail
        with:
          name: coverage-report
          path: coverage/

      # Publish test summary to Actions tab
      - name: Test summary
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Jest Tests
          path: coverage/junit.xml
          reporter: jest-junit
Jenkins — junit + publishHTML
// Jenkinsfile test stage with reporting
stage('Test') {
    steps {
        sh 'npm ci && npm test -- --coverage'
    }
    post {
        always {
            // Publish JUnit XML test results
            junit 'coverage/junit.xml'

            // Publish HTML coverage report
            publishHTML(target: [
                allowMissing:          false,
                alwaysLinkToLastBuild: true,
                keepAll:               true,
                reportDir:   'coverage/lcov-report',
                reportFiles: 'index.html',
                reportName:  'Coverage Report'
            ])
        }
        failure {
            echo '❌ Tests failed or coverage threshold not met'
        }
    }
}
💡 jest-junit for XML output
npm install --save-dev jest-junit
Add to jest.config.js:
reporters: ['default', 'jest-junit']
Generates coverage/junit.xml — consumed by Jenkins junit plugin and dorny/test-reporter.
Hands-On Lab

🔧 Tests + Coverage Gate

Install Jest → write 4 tests → 70% coverage threshold → watch CI fail when coverage drops → fix it

⏱ 30 minutes
my-devops-app ✓
Node.js installed ✓
🔧 Lab — Steps

Add tests and coverage gates

1
Install Jest + jest-junit
npm install --save-dev jest jest-junit. Add "test": "jest --coverage" to package.json scripts.
2
Create utility function (the code under test)
Create src/utils.js with add() and divide(). This is the business logic you'll test.
3
Write 4 unit tests
Create tests/utils.test.js with test cases for add (2 tests) and divide (2 tests, including the throw case). Run locally — all green.
4
Add jest.config.js with 70% threshold
Configure coverage collection, reporters (text + lcov + junit), and coverageThreshold at 70% for lines/branches/functions. Run — verify threshold passes.
5
Update CI to run tests with coverage
Update .github/workflows/ci.yml: add test step with npm test -- --coverage. Upload coverage artifact.
6
Test the gate: make coverage drop, watch CI fail
Add a new uncovered function to src/utils.js (without a test). Push. Watch CI fail on coverage threshold. Then add the test — CI turns green. ✅
🔧 Lab — Complete Code

All lab files

Setup + source files
# === Install ===
npm install --save-dev jest jest-junit

# === package.json scripts ===
# "scripts": { "test": "jest --coverage" }

# === src/utils.js ===
mkdir -p src
cat > src/utils.js << 'EOF'
function add(a, b) { return a + b; }

function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

function multiply(a, b) { return a * b; }

function isEven(n) { return n % 2 === 0; }

module.exports = { add, divide, multiply, isEven };
EOF

# === tests/utils.test.js ===
mkdir -p tests
cat > tests/utils.test.js << 'EOF'
const { add, divide, multiply, isEven } = require('../src/utils');

describe('add', () => {
  test('adds two numbers', () => expect(add(2, 3)).toBe(5));
  test('handles negatives', () => expect(add(-1, 1)).toBe(0));
});

describe('divide', () => {
  test('divides correctly', () => expect(divide(10, 2)).toBe(5));
  test('throws on zero', () => {
    expect(() => divide(5, 0)).toThrow('Division by zero');
  });
});

describe('multiply', () => {
  test('multiplies', () => expect(multiply(3, 4)).toBe(12));
});

describe('isEven', () => {
  test('even number', () => expect(isEven(4)).toBe(true));
  test('odd number', () => expect(isEven(3)).toBe(false));
});
EOF

# === Run tests locally ===
npm test
jest.config.js + CI workflow
# === jest.config.js ===
cat > jest.config.js << 'EOF'
module.exports = {
  collectCoverageFrom: ['src/**/*.js', '!src/index.js'],
  coverageProvider: 'v8',
  coverageReporters: ['text', 'lcov', 'junit', 'cobertura'],
  reporters: ['default', 'jest-junit'],
  coverageThreshold: {
    global: { lines: 70, branches: 70, functions: 70 }
  }
};
EOF

# === .github/workflows/test.yml ===
cat > .github/workflows/test.yml << 'EOF'
name: Tests

on:
  push:
    branches: [ main, 'feat/**' ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - name: Run tests with coverage
        run: npm test -- --coverage
      - name: Upload coverage
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-\${{ github.sha }}
          path: coverage/
EOF

# === Commit ===
git add .
git commit -m "test: add jest unit tests with 70% coverage gate"
git push origin main
💡 Test the gate — make it fail
Add to src/utils.js:
function secret() { return 'untested'; }
Push without a test for it. CI should fail coverage gate. Then write the test and watch CI go green.
Knowledge Check

Quiz Time

3 questions · 5 minutes · testing pyramid, branch coverage, quality gates

Day 13 knowledge check →
QUESTION 1 OF 3
Which level of the testing pyramid should have the most tests and run on every commit?
A
E2E tests — they test the full application flow
B
Integration tests — they test real component interactions
C
Unit tests — they test individual functions, run in milliseconds
D
Manual tests — they catch issues automation misses
QUESTION 2 OF 3
What does branch coverage measure?
A
How many lines of code are executed by tests
B
How many if/else branches and conditional paths are exercised by tests
C
How many Git branches the tests run on
D
How many functions are called during testing
QUESTION 3 OF 3
What is a quality gate in the context of a CI pipeline?
A
A security firewall that scans network traffic
B
An automated check that fails the build if quality metrics (like coverage) drop below a configured threshold
C
A manual code review step before merging
D
A GitHub branch protection rule requiring 2 approvals
Day 13 — Complete

What you learned today

🔺
Testing Pyramid
70% unit · 20% integration · 10% E2E. Most tests = unit. Most CI value = unit.
🧪
Jest
describe/test/expect. toBe, toEqual, toThrow. Mocks. test.each for data-driven.
📊
Coverage
Lines, branches, functions. 70–80% target. coverageThreshold = quality gate.
🚧
Gate in CI
Coverage drops → Jest exits 1 → CI fails → PR blocked. Quality enforced automatically.
Day 13 Action Items
  1. 4+ Jest tests passing locally ✓
  2. jest.config.js with 70% threshold ✓
  3. CI pipeline: coverage gate working (fail + fix tested) ✓
  4. Commit: test: add unit tests with coverage gate
Tomorrow — Day 14
Artifacts & Package Management

Write a Dockerfile, build a Docker image in CI, push to GitHub Container Registry (GHCR). Add npm audit for vulnerability scanning. Semantic versioning with git tags.

Dockerfile GHCR npm audit SemVer
📌 Reference

Jest complete cheatsheet

Test structure + lifecycle
// === Structure ===
describe('group name', () => {
  beforeAll(()  => { /* once before all */ });
  afterAll(()   => { /* once after all */ });
  beforeEach(() => { /* before each test */ });
  afterEach(()  => { /* after each test */ });

  test('description', () => {
    expect(value).toBe(expected);
  });

  it('alternative syntax', () => { /* same as test */ });
});

// === Skip / focus ===
test.skip('skip this test', () => {});
test.only('only run this', () => {});  // use with caution

// === Async tests ===
test('async with async/await', async () => {
  const result = await fetchUser(1);
  expect(result.name).toBe('Alice');
});

test('async with promise', () => {
  return fetchUser(1).then(user => {
    expect(user.name).toBe('Alice');
  });
});

// === Error testing ===
expect(() => riskyFunction()).toThrow();
expect(() => riskyFunction()).toThrow('specific message');
await expect(asyncFunc()).rejects.toThrow('error');
All matchers reference
// === Equality ===
.toBe(val)          // === strict equality
.toEqual(obj)       // deep equality (objects/arrays)
.toStrictEqual(obj) // like toEqual + checks undefined
.not.toBe(val)      // negation

// === Truthiness ===
.toBeTruthy()       .toBeFalsy()
.toBeNull()         .toBeUndefined()
.toBeDefined()

// === Numbers ===
.toBeGreaterThan(n)    .toBeLessThan(n)
.toBeGreaterThanOrEqual(n)
.toBeCloseTo(n, digits) // for floats

// === Strings ===
.toMatch(/regex/)
.toMatch('substring')

// === Arrays / Iterables ===
.toContain(item)
.toHaveLength(n)
.toEqual(expect.arrayContaining([a, b]))

// === Objects ===
.toHaveProperty('key')
.toHaveProperty('key', value)
.toMatchObject({ partial: 'match' })

// === Mocks ===
.toHaveBeenCalled()
.toHaveBeenCalledTimes(n)
.toHaveBeenCalledWith(arg1, arg2)
.toHaveBeenLastCalledWith(arg)
.toHaveReturnedWith(value)
📌 Good Test Patterns

Writing effective tests

AAA pattern + edge cases
// === AAA Pattern: Arrange → Act → Assert ===
test('createUser stores and returns new user', async () => {
  // ARRANGE — set up inputs and mocks
  const input = { name: 'Alice', email: 'alice@test.com' };
  db.save = jest.fn().mockResolvedValue({ id: 42, ...input });

  // ACT — call the function under test
  const result = await createUser(input);

  // ASSERT — verify the outcome
  expect(result.id).toBe(42);
  expect(result.name).toBe('Alice');
  expect(db.save).toHaveBeenCalledWith(input);
});

// === Test edge cases, not just happy path ===
describe('calculateDiscount', () => {
  test('applies 10% for orders over £100', () => {
    expect(calculateDiscount(120)).toBe(12);
  });
  test('applies no discount for orders under £100', () => {
    expect(calculateDiscount(80)).toBe(0);
  });
  test('handles exactly £100 (boundary)', () => {
    expect(calculateDiscount(100)).toBe(10);  // boundary value
  });
  test('throws for negative amounts', () => {
    expect(() => calculateDiscount(-10)).toThrow();
  });
  test('handles zero amount', () => {
    expect(calculateDiscount(0)).toBe(0);
  });
});
What makes a good test
Fast — runs in milliseconds
Isolated — doesn't depend on other tests
Repeatable — same result every time
Self-describing — test name explains what it tests
Specific — tests one thing, one assertion

The FIRST principles of good unit tests.
Test naming conventions

Format: [function] [scenario] [expected result]

  • "divide(a, 0) throws Division by zero"
  • "createUser returns 400 when email is missing"
  • "test 1"
  • "it works"
  • "adds numbers" (too vague — which numbers, what expected?)
⚠ Don't test implementation details
Test behaviour, not internals. If you test that a function calls a private method, your test breaks when you refactor — even if behaviour is unchanged. Test inputs → outputs, not how the code works internally.
📌 Beyond Jest

SonarQube & advanced quality gates

SonarQube — comprehensive quality

SonarQube analyses code and enforces quality on multiple dimensions:

  • Code coverage — integrates with your test coverage reports
  • Bugs — detects likely bugs (null dereferences, unclosed streams)
  • Vulnerabilities — OWASP top-10 security issues
  • Code smells — maintainability issues (duplication, complexity)
  • Technical debt — estimates time to fix all issues

Quality Gate: configurable policy — if any metric fails, the CI pipeline fails and the PR is blocked.

SonarQube in GitHub Actions
- name: SonarCloud Analysis
  uses: SonarSource/sonarcloud-github-action@master
  env:
    GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
    SONAR_TOKEN: \${{ secrets.SONAR_TOKEN }}
  with:
    args: |
      -Dsonar.projectKey=my-devops-app
      -Dsonar.coverage.exclusions=**/*.test.js
Other quality tools
  • Codecov / Coveralls — coverage tracking + PR comments showing coverage diff
  • ESLint security plugin — detects security issues in JS code
  • Dependabot — auto-PRs for outdated dependencies
  • npm audit — scan for known vulnerable packages in node_modules
  • Snyk — deep vulnerability scanning across code + dependencies
Quality gate hierarchy in a real pipeline
1. Lint (ESLint) — code style + basic errors
2. Unit tests + coverage threshold — functionality
3. Security scan (npm audit / Snyk) — vulnerabilities
4. SonarQube — comprehensive quality gate
5. Integration tests — real-world behaviour

All gates must pass before merging. Failure at any stage = block.
💡 Start simple
Don't implement all quality tools on Day 1. Start with: ESLint + Jest + coverageThreshold. Add npm audit. Add SonarQube when the team is comfortable. Quality is a journey, not a switch.
Week 3 Progress

Week 3 — 3 of 5 days complete

Day Topic Lab Output Status
Day 11CI/CD Conceptsci.yml live in Actions tab
Day 12Pipeline Deep DiveMulti-job matrix + Jenkinsfile parallel
Day 13 ← TODAYTesting in CIJest + 70% coverage gate in CI
Day 14Artifacts & DockerDocker image pushed to GHCR on mergeTomorrow
Day 15Continuous DeploymentStaging auto + prod approval gateFriday
Pipeline quality so far
✅ Trigger on push/PR
✅ Multi-job with dependencies
✅ Matrix testing (Node 18 + 20)
✅ Secrets management
✅ npm cache (60s → 5s)
Unit tests with coverage gate ← today

Still to add: Docker build/push, CD deploy
Your pipeline files so far
my-devops-app/
├── src/
│   └── utils.js         ← function under test
├── tests/
│   └── utils.test.js    ← Jest tests
├── jest.config.js       ← 70% threshold
├── .github/workflows/
│   ├── ci.yml           ← Day 11
│   ├── ci-advanced.yml  ← Day 12
│   └── test.yml         ← Day 13 ← new
└── Jenkinsfile
📌 Troubleshooting

Common testing issues & fixes

Problem Cause Fix
Jest: "Cannot find module"Wrong relative path in require()Check path: test in tests/ → require('../src/utils')
Coverage threshold not metUntested code in collectCoverageFromAdd tests for uncovered lines (shown red in HTML report). Or lower threshold temporarily.
CI: "npm test not found"No "test" script in package.jsonAdd "test": "jest --coverage" to scripts in package.json
Tests pass locally, fail in CIEnvironment-specific dependency or absolute pathUse relative paths. Mock environment variables. Ensure test is not time-dependent.
Async test always passesMissing async/await or returnAlways await async calls or return the promise in the test.
Tests leak state between runsMocks not reset between testsAdd beforeEach(() => jest.clearAllMocks())
coverage/junit.xml not foundjest-junit not installed or not in reportersnpm install --save-dev jest-junit. Add reporters: ['default', 'jest-junit'] to jest.config.js
Open handles warningDB connection or timer not closedAdd afterAll(() => server.close()). Use jest --detectOpenHandles
📌 Day 13 Reference

Everything at a glance

jest.config.js — full reference
module.exports = {
  // Files to test
  testMatch: ['**/__tests__/**/*.js', '**/*.test.js'],
  testPathIgnorePatterns: ['/node_modules/'],

  // Coverage settings
  collectCoverageFrom: ['src/**/*.js', '!src/index.js'],
  coverageProvider: 'v8',           // faster than babel
  coverageReporters: ['text', 'lcov', 'junit'],
  coverageDirectory: 'coverage',

  // QUALITY GATE
  coverageThreshold: {
    global: { lines: 70, branches: 70, functions: 70 }
  },

  // For Jenkins JUnit XML output
  reporters: ['default', 'jest-junit'],

  // Auto-clear mocks between tests
  clearMocks: true,         // clear mock.calls
  resetMocks: true,         // reset return values
  restoreMocks: true,       // restore spied methods

  // Timeout per test (ms)
  testTimeout: 10000,
};
test.yml — CI workflow
name: Tests
on:
  push:
    branches: [ main, 'feat/**' ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - name: Test + coverage gate
        run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage
          path: coverage/
💡 Day 13 commit messages
test: add Jest unit tests for utils module
ci: add coverage gate threshold to 70%
test: add missing tests to pass coverage gate