This document outlines the testing system for the Dialer Backend API. The project uses Jest as the testing framework with TypeScript support.
- Overview
- Running Tests
- Test Structure
- Writing Tests
- Mocking Patterns
- Test Helpers
- Coverage Reports
- Best Practices
- Common Testing Scenarios
The testing system is built on:
- Jest: JavaScript testing framework
- ts-jest: TypeScript preprocessor for Jest
- Test Helpers: Custom utilities for mocking database and Express objects
Tests are located in __tests__ directories adjacent to the code they test, or in files with .test.ts or .spec.ts extensions.
npm testWatch mode automatically re-runs tests when files change:
npm run test:watchGenerate coverage reports to see which parts of your code are tested:
npm run test:coverageCoverage reports are generated in the coverage/ directory. Open coverage/lcov-report/index.html in a browser to view the detailed coverage report.
Run a specific test file:
npm test -- formatResults.test.tsRun tests matching a pattern:
npm test -- --testNamePattern="encryptHash"Test files should follow one of these naming conventions:
*.test.ts- Standard test files*.spec.ts- Alternative naming convention- Located in
__tests__directories
Tests are organized to mirror the source code structure:
src/
├── functions/
│ ├── __tests__/
│ │ ├── formatResults.test.ts
│ │ └── encryptDecrypt.test.ts
│ ├── formatResults.ts
│ └── encryptDecrypt.ts
├── actions/
│ ├── __tests__/
│ │ └── GenericAction.test.ts
│ └── GenericAction.ts
└── __tests__/
└── helpers/
├── setup.ts
├── mockDatabase.ts
└── mockRequest.ts
import { functionToTest } from "../functionToTest";
describe("functionToTest", () => {
it("should do something specific", () => {
const result = functionToTest(input);
expect(result).toBe(expected);
});
});Use describe blocks to group related tests:
describe("MyClass", () => {
describe("methodA", () => {
it("should handle case 1", () => {
// test code
});
it("should handle case 2", () => {
// test code
});
});
});Use beforeEach, afterEach, beforeAll, and afterAll for setup and cleanup:
describe("MyTests", () => {
beforeAll(() => {
// Run once before all tests
});
beforeEach(() => {
// Run before each test
});
afterEach(() => {
// Run after each test
jest.clearAllMocks();
});
afterAll(() => {
// Run once after all tests
});
});Mock a function from another module:
jest.mock("../functions", () => ({
...jest.requireActual("../functions"),
encryptHash: jest.fn((value) => `encrypted_${value}`),
}));Use the test helpers to mock Sequelize models:
import { createMockModel } from "../../__tests__/helpers/mockDatabase";
jest.mock("../../models", () => ({
User: createMockModel({ id: 1, name: "Test User" }),
}));Use the test helpers to create mock Express objects:
import { createMockRequestResponse } from "../../__tests__/helpers/mockRequest";
const { req, res } = createMockRequestResponse({
body: { id: 1, name: "test" },
params: { userId: "123" },
query: { status: "active" },
});Set environment variables in tests:
beforeAll(() => {
process.env.OPENSSL_ALGORITHM = "aes-256-cbc";
process.env.OPENSSL_KEY = "test-key";
process.env.OPENSSL_IV = "test-iv";
});Creates a mock Sequelize instance:
import { createMockSequelize } from "../../__tests__/helpers/mockDatabase";
const mockSequelize = createMockSequelize();Creates a mock Sequelize model with common methods:
import { createMockModel } from "../../__tests__/helpers/mockDatabase";
const mockUserModel = createMockModel({ id: 1, name: "Test User" });Creates a mock Sequelize model instance:
import { createMockModelInstance } from "../../__tests__/helpers/mockDatabase";
const mockUser = createMockModelInstance({ id: 1, name: "Test User" });Creates a mock Express Request object:
import { createMockRequest } from "../../__tests__/helpers/mockRequest";
const req = createMockRequest({
body: { id: 1 },
params: { userId: "123" },
});Creates a mock Express Response object:
import { createMockResponse } from "../../__tests__/helpers/mockRequest";
const res = createMockResponse();Creates both request and response mocks:
import { createMockRequestResponse } from "../../__tests__/helpers/mockRequest";
const { req, res } = createMockRequestResponse({
body: { id: 1 },
});Extracts data from a mocked response:
import { getResponseData } from "../../__tests__/helpers/mockRequest";
const data = getResponseData(res);Gets the status code from a mocked response:
import { getResponseStatus } from "../../__tests__/helpers/mockRequest";
const status = getResponseStatus(res);After running npm run test:coverage, coverage reports are generated in the coverage/ directory:
- HTML Report:
coverage/lcov-report/index.html- Open in a browser for detailed coverage - LCOV Report:
coverage/lcov.info- For CI/CD integration - Text Summary: Displayed in the terminal
Coverage thresholds can be configured in jest.config.js:
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},Each test should be independent and not rely on the state of other tests:
beforeEach(() => {
jest.clearAllMocks();
});Use clear, descriptive test names that explain what is being tested:
// Good
it("should encrypt a string and return a hex string", () => {
// test
});
// Bad
it("encrypt works", () => {
// test
});Structure tests with clear sections:
it("should format a single object", async () => {
// Arrange
const input = { id: 1, name: "test" };
// Act
const result = await formatResults(input);
// Assert
expect(result).toBeDefined();
expect(result[0].id).toBe("encrypted_1");
});Don't just test the happy path. Test edge cases, error conditions, and boundary values:
it("should handle null input", async () => {
const result = await formatResults(null);
expect(result).toBeNull();
});
it("should handle empty array", async () => {
const result = await formatResults([]);
expect(result).toEqual([]);
});Mock external dependencies like database calls, API requests, and file system operations:
jest.mock("../../models", () => ({
User: createMockModel(),
}));Always use async/await or return promises in async tests:
it("should handle async operations", async () => {
const result = await asyncFunction();
expect(result).toBeDefined();
});import { formatPhone } from "../formatPhone";
describe("formatPhone", () => {
it("should format phone number correctly", async () => {
const result = await formatPhone("5551234567");
expect(result).toBe("(555) 123-4567");
});
});import GenericAction from "../GenericAction";
import { createMockRequestResponse } from "../../__tests__/helpers/mockRequest";
describe("GenericAction", () => {
it("should initialize correctly", () => {
const { req, res } = createMockRequestResponse();
const action = new GenericAction(req as any, res as any, false);
expect(action.req).toBe(req);
expect(action.res).toBe(res);
});
});import { createMockModel } from "../../__tests__/helpers/mockDatabase";
jest.mock("../../models", () => ({
User: createMockModel({ id: 1, name: "Test User" }),
}));
describe("User operations", () => {
it("should find user by id", async () => {
const user = await models.User.findByPk(1);
expect(user).toBeDefined();
expect(user.id).toBe(1);
});
});it("should handle errors gracefully", async () => {
const originalAlgorithm = process.env.OPENSSL_ALGORITHM;
delete process.env.OPENSSL_ALGORITHM;
const result = encryptHash("test");
expect(result).toBe("test");
process.env.OPENSSL_ALGORITHM = originalAlgorithm;
});The Jest configuration is in jest.config.js. Key settings:
- preset:
ts-jestfor TypeScript support - testEnvironment:
nodefor Node.js environment - testMatch: Patterns for test files
- setupFilesAfterEnv: Test setup file that runs before tests
- coverageDirectory: Where coverage reports are saved
- Ensure test files match the patterns in
jest.config.js - Check that
ts-jestis properly configured - Verify TypeScript compilation is working
- Ensure mocks are defined before imports
- Use
jest.clearAllMocks()inbeforeEachto reset mocks - Check that the module path in
jest.mock()matches the actual import path
- Always use
async/awaitor return promises - Increase timeout if needed:
jest.setTimeout(10000) - Use
awaitwhen calling async functions in tests