This document provides guidelines for writing and running tests in the woly-backend project.
The project uses Jest as the test framework with ts-jest for TypeScript support and Supertest for API integration testing.
The project enforces the following minimum coverage thresholds:
- Statements: 50%
- Branches: 50%
- Functions: 50%
- Lines: 50%
CI builds will fail if coverage drops below these thresholds.
Unit tests are located in __tests__/ directories alongside the source code:
controllers/
hosts.ts
__tests__/
hosts.unit.test.ts
services/
hostDatabase.ts
__tests__/
hostDatabase.unit.test.ts
Unit tests should:
- Test individual functions and classes in isolation
- Mock external dependencies (databases, network calls, file I/O)
- Focus on business logic and edge cases
- Be fast and independent
Integration tests are located in the root __tests__/ directory:
__tests__/
api.integration.test.ts
app.unit.test.ts
Integration tests should:
- Test complete request/response flows
- Use in-memory databases when possible
- Mock external services but not internal modules
- Verify API contracts and error handling
# Ensure the expected Node runtime
nvm use
# Optional: reinstall native deps after Node upgrade
npm rebuild
# Run all tests
npm test
# Run with coverage report
npm run test:coverage
# Run in watch mode (re-runs on file changes)
npm run test:watch
# Run only unit tests
npm run test:unit
# Run only integration tests
npm run test:integration
# Run in CI mode (used by GitHub Actions)
npm run test:ci- Node.js v24+ is supported.
.nvmrcprovides a baseline local version for consistency, but newer Node versions are supported.- Test preflight verifies that local socket bind is allowed because Supertest-based suites require it.
- If preflight fails on socket bind, run tests outside restricted/sandboxed execution environments.
import { functionToTest } from '../module';
describe('Module Name', () => {
describe('functionToTest', () => {
it('should do something specific', () => {
// Arrange
const input = 'test';
// Act
const result = functionToTest(input);
// Assert
expect(result).toBe('expected');
});
});
});// Mock external module
jest.mock('axios');
jest.mock('../services/database');
// Mock specific functions
const mockGetData = jest.fn();
jest.mock('../api', () => ({
getData: mockGetData,
}));
// In test
beforeEach(() => {
jest.clearAllMocks();
mockGetData.mockResolvedValue({ data: 'test' });
});import { Request, Response } from 'express';
import { controllerFunction } from '../controller';
let mockReq: Partial<Request>;
let mockRes: Partial<Response>;
beforeEach(() => {
mockReq = {
params: { id: '123' },
body: { name: 'test' },
query: {},
};
mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
};
});
it('should handle request', async () => {
await controllerFunction(mockReq as Request, mockRes as Response);
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({ success: true });
});import request from 'supertest';
import express from 'express';
import routes from '../routes';
let app: express.Application;
beforeAll(() => {
app = express();
app.use(express.json());
app.use('/api', routes);
});
it('should return hosts list', async () => {
const response = await request(app).get('/api/hosts').expect(200).expect('Content-Type', /json/);
expect(response.body).toHaveProperty('hosts');
expect(Array.isArray(response.body.hosts)).toBe(true);
});it('should handle async operations', async () => {
// Using async/await
const result = await asyncFunction();
expect(result).toBe('expected');
});
it('should handle promises', () => {
// Using return
return asyncFunction().then((result) => {
expect(result).toBe('expected');
});
});
it('should handle rejections', async () => {
// Testing errors
await expect(asyncFunction()).rejects.toThrow('Error message');
});it('should handle validation errors', () => {
expect(() => {
validateInput('invalid');
}).toThrow('Validation failed');
});
it('should return error response', async () => {
mockDb.getUser.mockRejectedValue(new Error('Not found'));
await request(app)
.get('/users/999')
.expect(404)
.expect((res) => {
expect(res.body).toHaveProperty('error');
expect(res.body.error.code).toBe('NOT_FOUND');
});
});✅ Write descriptive test names that explain what is being tested
✅ Use describe blocks to group related tests
✅ Clear all mocks between tests using jest.clearAllMocks()
✅ Test edge cases and error conditions
✅ Use beforeEach for common setup
✅ Make tests independent and order-agnostic
✅ Mock external dependencies (databases, APIs, file system)
✅ Test one thing per test case
✅ Use meaningful assertion messages
✅ Keep tests simple and readable
❌ Test implementation details ❌ Share state between tests ❌ Make network calls in unit tests ❌ Use real databases in unit tests ❌ Write tests that depend on execution order ❌ Mock everything (test real integration where appropriate) ❌ Ignore failing tests ❌ Skip writing tests for "trivial" code ❌ Commit code without running tests locally ❌ Remove tests to increase coverage (fix the code instead)
Coverage is configured in jest.config.js:
coverageThreshold: {
global: {
branches: 50,
functions: 50,
lines: 50,
statements: 50
}
}app.ts- Server entry point (initialization code)swagger.ts- API documentation configuration- Type definition files (
*.d.ts) - Test files themselves
- Build artifacts and dependencies
After running npm run test:coverage:
- Terminal: Summary displayed in console
- HTML: Open
coverage/lcov-report/index.htmlin browser - Codecov: Automatic upload in CI (view on GitHub)
Tests run automatically on:
- Every push to
mainwhen automatic CI is enabled - Every pull request
The CI pipeline:
- Installs dependencies
- Runs linter
- Runs tests with coverage
- Uploads coverage to Codecov
- Fails build if coverage drops below thresholds
- Use mocks for database and network calls
- Use in-memory databases for integration tests
- Run specific test files:
npm test -- path/to/test.ts - Use
test.onlytemporarily to run single test
- Delete
coverage/directory - Run
npm run test:coverageagain - Check
.gitignoredoesn't exclude your files
- Ensure
jest.clearAllMocks()is inbeforeEach - Check mock is defined before the import that uses it
- Verify mock path matches module path exactly
- Check environment-specific code paths
- Verify timing-dependent tests have sufficient timeouts
- Ensure no reliance on local files or state
- Check for timezone-dependent date handling
If you have questions about testing:
- Check this documentation first
- Look at existing tests for examples
- Search Jest documentation
- Ask in team chat or pull request
- Open an issue on GitHub