Skip to main content

Overview

EPR LAPS Backend uses Vitest as the test framework with V8 coverage for code coverage reporting.
All tests are configured to run in UTC timezone (TZ=UTC) to ensure consistent date/time handling.

Running Tests

Test Commands

npm test
Available scripts from package.json:
{
  "scripts": {
    "test": "TZ=UTC vitest run --coverage",
    "test:watch": "TZ=UTC vitest"
  }
}

Test Modes

1

Single Run (CI Mode)

Run all tests once and generate coverage report:
npm test
This is the default mode for CI/CD pipelines.
2

Watch Mode (Development)

Run tests in watch mode with hot reload:
npm run test:watch
Tests automatically re-run when files change.

Test Configuration

Test configuration is defined in vitest.config.js:
import { defineConfig, configDefaults } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    clearMocks: true,
    coverage: {
      provider: 'v8',
      reportsDirectory: './coverage',
      reporter: ['text', 'lcov'],
      include: ['src/**'],
      exclude: [...configDefaults.exclude, 'coverage']
    },
    setupFiles: ['.vite/mongo-memory-server.js', '.vite/setup-files.js']
  }
})

Configuration Options

Global Settings:
  • globals: true - Enables global test APIs (describe, it, expect) without imports
  • environment: 'node' - Runs tests in Node.js environment
  • clearMocks: true - Automatically clears mock state between tests
Setup Files:
  • .vite/mongo-memory-server.js - Configures in-memory MongoDB for tests
  • .vite/setup-files.js - Additional test setup and global configuration

Coverage Reports

Coverage Configuration

coverage: {
  provider: 'v8',
  reportsDirectory: './coverage',
  reporter: ['text', 'lcov'],
  include: ['src/**'],
  exclude: [...configDefaults.exclude, 'coverage']
}
Settings:
  • Provider: V8 (built into Node.js, fast and accurate)
  • Output Directory: ./coverage
  • Reporters:
    • text - Console output with coverage summary
    • lcov - HTML report and lcov.info for CI tools
  • Include: All files in src/**
  • Exclude: Default exclusions plus coverage directory

Viewing Coverage

1

Run tests with coverage

npm test
2

View console summary

Coverage summary is displayed in the terminal:
% Coverage report from v8
--------|---------|---------|---------|---------
File    | % Stmts | % Branch| % Funcs | % Lines 
--------|---------|---------|---------|---------
All     |   85.23 |   78.45 |   92.11 |   85.23
--------|---------|---------|---------|---------
3

Open HTML report

Open the detailed HTML coverage report:
open coverage/lcov-report/index.html
Or on Linux:
xdg-open coverage/lcov-report/index.html
The HTML coverage report provides interactive line-by-line coverage visualization, making it easy to identify untested code.

MongoDB Test Setup

Tests use vitest-mongodb with MongoDB Memory Server for isolated database testing. From package.json devDependencies:
{
  "vitest-mongodb": "1.0.3"
}

Using MongoDB in Tests

import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { MongoClient } from 'mongodb'

let client
let db

beforeAll(async () => {
  // MongoDB Memory Server is configured in setup files
  client = new MongoClient(global.__MONGO_URI__)
  await client.connect()
  db = client.db(global.__MONGO_DB_NAME__)
})

afterAll(async () => {
  await client.close()
})

describe('Database operations', () => {
  it('should insert and retrieve data', async () => {
    const collection = db.collection('test')
    
    await collection.insertOne({ name: 'Test' })
    const result = await collection.findOne({ name: 'Test' })
    
    expect(result.name).toBe('Test')
  })
})

Mocking

HTTP Request Mocking

Tests use vitest-fetch-mock for mocking fetch requests: From package.json devDependencies:
{
  "vitest-fetch-mock": "0.4.5"
}
Example:
import { describe, it, expect, vi } from 'vitest'
import { fetch } from 'undici'

vi.mock('undici', () => ({
  fetch: vi.fn()
}))

describe('API client', () => {
  it('should fetch data from external API', async () => {
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ data: 'test' })
    })
    
    const result = await myApiClient.getData()
    
    expect(fetch).toHaveBeenCalledWith('http://localhost:3001/data')
    expect(result).toEqual({ data: 'test' })
  })
})

Auto-Clearing Mocks

Mocks are automatically cleared between tests due to clearMocks: true in the config:
// No need to manually clear mocks
describe('Test suite', () => {
  it('test 1', () => {
    const fn = vi.fn()
    fn()
    expect(fn).toHaveBeenCalledTimes(1)
  })
  
  it('test 2', () => {
    // Mock is automatically cleared
    const fn = vi.fn()
    expect(fn).not.toHaveBeenCalled()
  })
})

Writing Tests

Test Structure

import { describe, it, expect } from 'vitest'

describe('Feature name', () => {
  it('should do something', () => {
    const result = myFunction()
    expect(result).toBe(expected)
  })
})

Global Test APIs

With globals: true, you don’t need to import test APIs:
// No imports needed for:
// describe, it, expect, beforeEach, afterEach, beforeAll, afterAll, vi

describe('My feature', () => {
  it('works', () => {
    expect(true).toBe(true)
  })
})

Best Practices

1

Write descriptive test names

// Good
it('should return 404 when organisation does not exist', async () => {})

// Bad
it('returns error', async () => {})
2

Test one thing per test

// Good - focused test
it('should create bank details', async () => {
  const result = await createBankDetails(data)
  expect(result.success).toBe(true)
})

it('should validate required fields', async () => {
  await expect(createBankDetails({})).rejects.toThrow()
})

// Bad - testing multiple things
it('should handle bank details', async () => {
  // Tests creation, validation, updating, deletion...
})
3

Use setup and teardown hooks

describe('Database tests', () => {
  beforeEach(async () => {
    await seedDatabase()
  })
  
  afterEach(async () => {
    await cleanDatabase()
  })
  
  it('test with clean state', async () => {
    // Each test starts with fresh seeded data
  })
})
4

Mock external dependencies

import { vi } from 'vitest'

vi.mock('./external-api', () => ({
  fetchData: vi.fn().mockResolvedValue({ data: 'test' })
}))

// Now test your code in isolation

Continuous Integration

In CI/CD pipelines, run tests with coverage:
# Example GitHub Actions
steps:
  - name: Run tests
    run: npm test
  
  - name: Upload coverage
    uses: codecov/codecov-action@v3
    with:
      files: ./coverage/lcov.info
The lcov.info file in the coverage directory is compatible with most CI coverage tools like Codecov, Coveralls, and SonarCloud.

Debugging Tests

Run Specific Tests

# Run tests in specific file
npm test src/api/bank-details.test.js

# Run tests matching pattern
npm test -- --grep "bank details"

Debug with Node Inspector

node --inspect-brk node_modules/.bin/vitest run
Then open chrome://inspect in Chrome to debug.

Troubleshooting

Tests Hang or Timeout

Ensure all async operations are properly awaited and resources are cleaned up:
afterEach(async () => {
  await server.stop()
  await db.close()
})

MongoDB Connection Issues

Verify MongoDB Memory Server is configured correctly in .vite/mongo-memory-server.js.

Coverage Not Generated

Ensure files are in the src/** directory and not excluded by the configuration.

Next Steps

Local Setup

Set up your development environment

Configuration

Learn about configuration options

Build docs developers (and LLMs) love