The QuickLendX backend implements comprehensive testing strategies including unit tests, integration tests, and contract tests. This guide covers all testing approaches and best practices.
src/
├── config/
│ ├── __tests__/
│ │ ├── loader.test.ts
│ │ └── masking.test.ts
│ ├── index.ts
│ ├── loader.ts
│ ├── masking.ts
│ └── schema.ts
└── testing/
├── __tests__/
│ ├── contract-validator.test.ts
│ └── contract-harness.test.ts
├── fixtures/
│ ├── auth.fixtures.ts
│ ├── user.fixtures.ts
│ ├── invoice.fixtures.ts
│ ├── bid.fixtures.ts
│ └── system.fixtures.ts
├── contract-validator.ts
└── contract-harness.ts
npm testnpm run test:watchnpm run test:coveragenpm test src/config/__tests__/loader.test.tsnpm test -- --grep "ContractValidator"Contract testing validates that API responses strictly adhere to the OpenAPI specification. This prevents breaking changes and ensures API consistency.
- Prevents Breaking Changes: Catches schema violations before deployment
- Documentation as Tests: OpenAPI spec serves as single source of truth
- Fail-Fast: Tests fail immediately on contract violations
- Clear Error Messages: Shows exactly what doesn't match the spec
The API contract is defined in openapi.yaml at the project root. This file:
- Defines all endpoints, methods, and status codes
- Specifies request/response schemas
- Documents data types, formats, and constraints
- Serves as the contract between frontend and backend
The contract testing harness provides utilities for validating API responses:
import { createContractHarness } from './testing/contract-harness';
// Create harness instance
const harness = createContractHarness({
specPath: './openapi.yaml', // Path to OpenAPI spec
failFast: true, // Throw on first failure
verbose: false, // Log each test result
});
// Test a response
const result = harness.testResponse(
'POST', // HTTP method
'/auth/login', // Endpoint path
200, // Status code
responseBody // Actual response data
);
// Check result
if (!result.passed) {
console.error('Contract violation:', result.validation.errors);
}# Run all contract tests
npm test src/testing/__tests__/contract-validator.test.ts
# Run with coverage
npm run test:coverage -- src/testing/__tests__/contract-validator.test.ts
# Watch mode for development
npm run test:watch -- src/testing/__tests__/contract-validator.test.tsimport { createContractHarness } from '../testing/contract-harness';
import { validLoginResponse } from '../testing/fixtures/auth.fixtures';
const harness = createContractHarness();
// This should pass
const result = harness.testResponse(
'POST',
'/auth/login',
200,
validLoginResponse
);
expect(result.passed).toBe(true);// Missing required field
const invalidResponse = {
token: 'some-token',
// Missing 'user' field
};
const result = harness.testResponse(
'POST',
'/auth/login',
200,
invalidResponse
);
expect(result.passed).toBe(false);
expect(result.validation.errors).toContainEqual(
expect.objectContaining({
path: 'user',
message: 'Required property missing',
})
);const invalidResponse = {
token: 12345, // Should be string
user: { /* ... */ },
};
const result = harness.testResponse(
'POST',
'/auth/login',
200,
invalidResponse
);
expect(result.passed).toBe(false);
expect(result.validation.errors).toContainEqual(
expect.objectContaining({
path: 'token',
message: 'Type mismatch',
expected: 'string',
actual: 'number',
})
);Fixtures provide consistent test data for contract tests. They're organized by domain:
import { validLoginResponse, invalidCredentialsError } from './fixtures/auth.fixtures';
// Use in tests
harness.testResponse('POST', '/auth/login', 200, validLoginResponse);
harness.testResponse('POST', '/auth/login', 401, invalidCredentialsError);import { validInvoice, validInvoiceListResponse } from './fixtures/invoice.fixtures';
harness.testResponse('POST', '/invoices', 201, validInvoice);
harness.testResponse('GET', '/invoices', 200, validInvoiceListResponse);When adding new endpoints:
- Create fixture file in
src/testing/fixtures/ - Export valid and invalid response examples
- Ensure fixtures match OpenAPI schema
- Use realistic but fake data
Example:
// src/testing/fixtures/payment.fixtures.ts
export const validPayment = {
id: '123e4567-e89b-12d3-a456-426614174000',
amount: '1000.00',
currency: 'USDC',
status: 'completed',
createdAt: '2024-01-25T10:00:00Z',
};
export const paymentNotFoundError = {
error: 'NOT_FOUND',
message: 'Payment not found',
};When the OpenAPI spec changes:
- Update the spec in
openapi.yaml - Update fixtures to match new schema
- Run contract tests to verify changes
- Update integration tests if needed
Example workflow:
# 1. Edit openapi.yaml
vim openapi.yaml
# 2. Update fixtures
vim src/testing/fixtures/invoice.fixtures.ts
# 3. Run tests to verify
npm test src/testing/__tests__/contract-validator.test.ts
# 4. Check coverage
npm run test:coverageWhen contract tests fail, you have two options:
If the response is wrong, fix the implementation:
// Before (wrong)
return { data: invoices };
// After (correct per spec)
return {
data: invoices,
total: invoices.length,
limit: 20,
offset: 0,
};If the spec needs to change (breaking change):
- Document the breaking change in CHANGELOG
- Update OpenAPI spec with new schema
- Update fixtures to match
- Version the API if needed (e.g.,
/api/v2) - Notify frontend team of changes
Contract tests run automatically in CI/CD:
# .github/workflows/test.yml
- name: Run Contract Tests
run: |
npm test src/testing/__tests__/contract-validator.test.ts
npm test src/testing/__tests__/contract-harness.test.ts
- name: Check Coverage
run: |
npm run test:coverage
# Fail if coverage < 95%- Test All Endpoints: Every endpoint should have contract tests
- Test All Status Codes: Test success and error responses
- Use Fixtures: Don't inline test data, use fixtures
- Test Edge Cases: Empty arrays, null values, optional fields
- Keep Fixtures Realistic: Use valid UUIDs, dates, emails
- Update Together: Keep spec, fixtures, and tests in sync
- Fail Fast: Use
failFast: truein CI/CD - No Real Secrets: Use mock tokens and test credentials
Contract tests verify that sensitive data is never exposed:
it('should not expose sensitive data in error messages', () => {
const responseWithSecrets = {
token: 'super-secret-jwt-token-12345',
user: { id: 'invalid-uuid', /* ... */ },
};
try {
harness.testResponse('POST', '/auth/login', 200, responseWithSecrets);
} catch (error) {
const errorMessage = error.message;
// Should mention the field
expect(errorMessage).toContain('id');
// Should NOT contain the secret
expect(errorMessage).not.toContain('super-secret-jwt-token-12345');
}
});Contract tests use mock authentication:
// Don't use real tokens
const mockToken = 'mock-jwt-token-for-testing';
// Don't use real credentials
const testCredentials = {
email: 'test@example.com',
password: 'TestPassword123!',
};Tests run in isolated environment:
beforeEach(() => {
process.env.NODE_ENV = 'test';
// No production secrets required
});it('should load valid configuration', () => {
process.env = {
DATABASE_URL: 'postgresql://localhost:5432/testdb',
JWT_SECRET: 'test-secret-minimum-32-characters',
// ... other required vars
};
const config = loadConfig();
expect(config.DATABASE_URL).toBe('postgresql://localhost:5432/testdb');
});it('should fail when required field is missing', () => {
process.env = {
// Missing DATABASE_URL
JWT_SECRET: 'test-secret',
};
loadConfig();
expect(process.exit).toHaveBeenCalledWith(1);
});it('should redact sensitive values', () => {
const config = {
PORT: 3000,
JWT_SECRET: 'super-secret',
};
const safe = getSafeConfig(config);
expect(safe.JWT_SECRET).toBe('[REDACTED]');
expect(safe.PORT).toBe(3000);
});All modules must maintain at least 95% test coverage:
npm run test:coverage
# Output:
# File | % Stmts | % Branch | % Funcs | % Lines
# --------------------|---------|----------|---------|--------
# All files | 97.5 | 95.2 | 98.1 | 97.8
# config/ | 98.2 | 96.5 | 100 | 98.5
# testing/ | 96.8 | 94.0 | 96.2 | 97.1Coverage reports are generated in coverage/:
coverage/index.html- HTML report (open in browser)coverage/lcov.info- LCOV format (for CI tools)coverage/coverage-summary.json- JSON summary
If coverage is below 95%:
-
Identify uncovered lines:
npm run test:coverage open coverage/index.html
-
Add missing tests for:
- Edge cases
- Error paths
- Boundary conditions
- Type validations
-
Verify improvement:
npm run test:coverage
- Unit tests:
*.test.tsnext to source file - Integration tests:
__tests__/*.test.tsin module directory - Fixtures:
fixtures/*.fixtures.ts
describe('ModuleName', () => {
describe('functionName', () => {
it('should handle valid input', () => {
// Arrange
const input = 'valid';
// Act
const result = functionName(input);
// Assert
expect(result).toBe('expected');
});
it('should handle invalid input', () => {
expect(() => functionName('invalid')).toThrow();
});
});
});describe('TestSuite', () => {
beforeAll(() => {
// Run once before all tests
});
beforeEach(() => {
// Run before each test
resetConfig();
});
afterEach(() => {
// Run after each test
vi.clearAllMocks();
});
afterAll(() => {
// Run once after all tests
});
});npm test -- --grep "should validate login response"Add to .vscode/launch.json:
{
"type": "node",
"request": "launch",
"name": "Debug Tests",
"runtimeExecutable": "npm",
"runtimeArgs": ["test", "--", "--no-coverage"],
"console": "integratedTerminal"
}npm test -- --reporter=verbosename: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Check coverage
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info// package.json
{
"husky": {
"hooks": {
"pre-commit": "npm test",
"pre-push": "npm run test:coverage"
}
}
}- Clear node_modules:
rm -rf node_modules && npm ci - Clear test cache:
npm test -- --clearCache - Check Node version:
node --version(should be 20+)
- Check environment variables
- Verify Node version matches
- Check for timing issues (use
vi.useFakeTimers()) - Review CI logs for specific errors
- Clear coverage directory:
rm -rf coverage - Run with clean cache:
npm test -- --clearCache --coverage
- Write Tests First (TDD) - Define behavior before implementation
- Test Behavior, Not Implementation - Focus on what, not how
- Keep Tests Simple - One assertion per test when possible
- Use Descriptive Names - Test names should explain what they test
- Avoid Test Interdependence - Each test should run independently
- Mock External Dependencies - Don't call real APIs or databases
- Test Edge Cases - Null, undefined, empty, boundary values
- Maintain Fixtures - Keep test data organized and reusable
- Review Coverage - Aim for 95%+ coverage
- Update Tests with Code - Keep tests in sync with implementation
Last Updated: 2024-01-25