8.6 KiB
Testing Framework Guide for Butler SOS
This document provides a comprehensive guide for Butler SOS developers on how the testing framework is set up, how to write tests, and how to run them effectively.
Overview
Butler SOS uses Jest v30.0.5 as its primary testing framework. The project is configured to work with ES modules and provides comprehensive test coverage across all major components.
Test Framework Configuration
Jest Configuration
The Jest configuration is defined in jest.config.mjs with the following key settings:
- Test Environment:
node- All tests run in a Node.js environment - ES Module Support: Fully configured for ES modules with
type: "module"in package.json - Coverage Collection: Enabled by default with V8 coverage provider
- Coverage Directory:
./coverage/ - Clear Mocks: Automatically clears mocks between tests
Package.json Scripts
The following npm scripts are available for testing:
# Run Jest tests only
npm run jest
# Run full test suite (Jest + Snyk + formatting)
npm run test
# Run only unit tests (alias for jest)
npm run test:unit
ES Module Configuration
Butler SOS runs Jest with specific Node.js flags to support ES modules:
node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js
This configuration:
- Enables experimental VM modules support
- Suppresses warnings for experimental features
- Allows Jest to work seamlessly with ES modules
Test File Organization
Directory Structure
Tests are organized using the __tests__ directory pattern throughout the codebase:
src/
├── lib/
│ └── __tests__/
│ ├── heartbeat.test.js
│ ├── post-to-mqtt.test.js
│ ├── config-file-schema.test.js
│ └── ...
├── lib/config-schemas/
│ └── __tests__/
│ ├── destinations.test.js
│ ├── credentials.test.js
│ └── ...
├── lib/udp_handlers/user_events/
│ └── __tests__/
│ └── message-event.test.js
└── plugins/
└── __tests__/
├── sensible.test.js
└── support.test.js
Naming Conventions
- Test files use the
.test.jsextension - Test files are named after the module they test (e.g.,
heartbeat.js→heartbeat.test.js) - Test files are placed in
__tests__directories alongside or near the code they test
Writing Tests
Basic Test Structure
All test files follow this ES module import pattern:
import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals';
import { moduleToTest } from '../module-to-test.js';
describe('module-to-test', () => {
test('should do something', () => {
// Test implementation
expect(result).toBe(expected);
});
});
Mocking Dependencies
Butler SOS uses Jest's ES module mocking capabilities. Here are the common patterns:
External Dependencies
// Mock external modules before importing them
jest.unstable_mockModule('axios', () => ({
default: {
get: jest.fn(),
post: jest.fn(),
},
}));
// Import after mocking
const axios = (await import('axios')).default;
Internal Modules
// Mock internal globals module
jest.unstable_mockModule('../../globals.js', () => ({
default: {
logger: {
error: jest.fn(),
debug: jest.fn(),
verbose: jest.fn(),
},
config: {
get: jest.fn(),
has: jest.fn(),
},
},
}));
const globals = (await import('../../globals.js')).default;
Configuration Testing
Many tests verify configuration schema validation:
import Ajv from 'ajv';
import addKeywords from 'ajv-keywords';
import addFormats from 'ajv-formats';
import configFileSchema from '../config-file-schema.js';
describe('config-file-schema', () => {
let ajv;
beforeEach(() => {
ajv = new Ajv({
strict: true,
allErrors: true,
allowUnionTypes: true,
});
addKeywords(ajv, ['transform']);
addFormats(ajv);
});
test('should validate against minimal config', () => {
const minimalConfig = { /* config object */ };
const validate = ajv.compile(configFileSchema);
const valid = validate(minimalConfig);
expect(valid).toBe(true);
});
});
Running Tests
Running All Tests
# Run all tests with coverage
npm run test
# Run only Jest tests
npm run jest
npm run test:unit
Running Specific Tests
# Run tests matching a pattern
npx jest heartbeat
# Run tests in a specific directory
npx jest src/lib/__tests__/
# Run a specific test file
npx jest src/lib/__tests__/heartbeat.test.js
Running Tests with Options
# Run tests in watch mode
npx jest --watch
# Run tests with verbose output
npx jest --verbose
# Run tests with coverage report
npx jest --coverage
Test Coverage
Coverage Configuration
Coverage is automatically collected during test runs and includes:
- Provider: V8 (native Node.js coverage)
- Output Directory:
./coverage/ - Report Format: Multiple formats including HTML, text, and LCOV
Viewing Coverage Reports
After running tests, coverage reports are available in:
coverage/
├── lcov-report/
│ └── index.html # Detailed HTML coverage report
├── lcov.info # Machine-readable coverage data
└── coverage-final.json
Open coverage/lcov-report/index.html in a browser to view detailed coverage information.
Jest v30 Specific Considerations
ES Module Support
Jest v30 has improved ES module support but requires specific configuration:
-
No
--runInBandFlag: Jest v30 has stricter ES module handling when running tests in band mode. Tests run in parallel by default. -
Dynamic Imports: All dynamic imports (
import()) in test files are strictly validated. -
Module Mocking: Use
jest.unstable_mockModule()for ES module mocking before importing modules.
Breaking Changes from v29
- Removed
--runInBandfrom npm scripts due to ES module compatibility issues - Stricter dynamic import validation
- Improved parallel test execution performance
Test Categories
Unit Tests
Located throughout the codebase, these test individual modules:
- Configuration validation (
config-*.test.js) - Data processing (
post-to-*.test.js) - Event handling (
*-event.test.js) - Utility functions (
heartbeat.test.js,service_uptime.test.js)
Integration Tests
Tests that verify module interactions:
- Plugin loading (
plugins/__tests__/) - Configuration schemas (
config-schemas/__tests__/) - Event message handling (
udp_handlers/__tests__/)
Best Practices
Test Organization
- Keep tests close to code: Place tests in
__tests__directories near the code they test - One test file per module: Each module should have a corresponding test file
- Descriptive test names: Use clear, descriptive test and describe block names
Mocking Strategy
- Mock external dependencies: Always mock HTTP clients, databases, and external services
- Mock global state: Mock the globals module for consistent test isolation
- Use beforeEach for setup: Initialize mocks and test data in beforeEach blocks
Test Structure
- Arrange, Act, Assert: Structure tests with clear setup, execution, and verification phases
- Test edge cases: Include tests for error conditions and boundary cases
- Keep tests focused: Each test should verify one specific behavior
Performance
- Parallel execution: Jest v30 runs tests in parallel by default for better performance
- Selective test runs: Use Jest's pattern matching to run only relevant tests during development
- Coverage optimization: Focus coverage collection on areas under active development
Troubleshooting
Common Issues
- Module not found errors: Ensure all imports use correct file extensions (
.js) - ES module import errors: Check that mocks are set up before imports
- Timeout issues: Increase Jest timeout for long-running async tests
Debug Tips
- Use
--verboseflag: Get detailed output for failing tests - Add
console.log: Temporary debugging in test files - Run single tests: Isolate failing tests to debug specific issues
Continuous Integration
Tests run automatically in CI/CD pipelines with:
- Full test suite execution
- Coverage report generation
- Security scanning with Snyk
- Code formatting validation
The test suite must pass before any code can be merged to main branches.