A step-by-step guide for contributors on how to write tests for express-swagger-auto.
Ensure you have the development dependencies installed:
pnpm installPlace your test files according to the module being tested:
| Module Type | Test Location |
|---|---|
| Core modules | src/core/__tests__/*.test.ts |
| Parser modules | src/parsers/__tests__/*.test.ts |
| Validator modules | src/validators/*.test.ts |
| Middleware | src/middleware/*.test.ts |
| CLI | test/cli/*.test.ts |
Test individual functions or classes in isolation.
import { describe, it, expect } from 'vitest';
import { PathParameterExtractor } from '../PathParameterExtractor';
describe('PathParameterExtractor', () => {
const extractor = new PathParameterExtractor();
it('should extract single path parameter', () => {
const result = extractor.extractPathParameters('/users/:id');
expect(result.parameters).toHaveLength(1);
expect(result.parameters[0].name).toBe('id');
expect(result.parameters[0].in).toBe('path');
expect(result.parameters[0].required).toBe(true);
});
});Test how multiple modules work together.
import { describe, it, expect } from 'vitest';
import express from 'express';
import { RouteDiscovery } from '../core/RouteDiscovery';
import { SpecGenerator } from '../core/SpecGenerator';
describe('Route Discovery and Spec Generation Integration', () => {
it('should generate spec from discovered routes', () => {
const app = express();
app.get('/users', (req, res) => res.json([]));
app.post('/users', (req, res) => res.json({}));
const discovery = new RouteDiscovery();
const routes = discovery.discover(app);
const generator = new SpecGenerator({
info: { title: 'Test API', version: '1.0.0' },
});
const spec = generator.generate(routes);
expect(spec.paths['/users'].get).toBeDefined();
expect(spec.paths['/users'].post).toBeDefined();
});
});Test complex output by comparing to saved snapshots.
import { describe, it, expect } from 'vitest';
import { SpecGenerator } from '../core/SpecGenerator';
describe('SpecGenerator Snapshots', () => {
it('should generate consistent spec structure', () => {
const generator = new SpecGenerator({
info: { title: 'API', version: '1.0.0' },
});
const spec = generator.generate([
{ method: 'GET', path: '/users', handler: () => {} },
]);
expect(spec).toMatchSnapshot();
});
});// src/validators/CustomAdapter.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { CustomAdapter } from './CustomAdapter';
describe('CustomAdapter', () => {
let adapter: CustomAdapter;
beforeEach(() => {
adapter = new CustomAdapter();
});
describe('Schema Conversion', () => {
it('should convert string schema', () => {
const customSchema = { type: 'text' };
const openAPISchema = adapter.convert(customSchema);
expect(openAPISchema).toEqual({
type: 'string',
});
});
it('should convert number schema with constraints', () => {
const customSchema = {
type: 'number',
min: 0,
max: 100,
};
const openAPISchema = adapter.convert(customSchema);
expect(openAPISchema).toEqual({
type: 'number',
minimum: 0,
maximum: 100,
});
});
it('should handle nested object schemas', () => {
const customSchema = {
type: 'object',
fields: {
name: { type: 'text', required: true },
age: { type: 'number' },
},
};
const openAPISchema = adapter.convert(customSchema);
expect(openAPISchema.type).toBe('object');
expect(openAPISchema.properties.name.type).toBe('string');
expect(openAPISchema.required).toContain('name');
});
});
describe('Error Handling', () => {
it('should throw on invalid schema', () => {
expect(() => adapter.convert(null)).toThrow();
});
it('should return generic schema for unknown types', () => {
const schema = adapter.convert({ type: 'unknown' });
expect(schema).toEqual({ type: 'object' });
});
});
});// src/core/__tests__/RouteDiscovery.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { RouteDiscovery } from '../RouteDiscovery';
import express, { Router } from 'express';
describe('RouteDiscovery', () => {
let discovery: RouteDiscovery;
beforeEach(() => {
discovery = new RouteDiscovery();
});
describe('Simple Routes', () => {
it('should discover GET route', () => {
const app = express();
app.get('/users', (req, res) => res.json([]));
const routes = discovery.discover(app);
expect(routes).toHaveLength(1);
expect(routes[0].method).toBe('GET');
expect(routes[0].path).toBe('/users');
});
it('should discover multiple methods on same path', () => {
const app = express();
app.get('/items', (req, res) => res.json([]));
app.post('/items', (req, res) => res.json({}));
const routes = discovery.discover(app);
expect(routes).toHaveLength(2);
expect(routes.map(r => r.method)).toContain('GET');
expect(routes.map(r => r.method)).toContain('POST');
});
});
describe('Nested Routers', () => {
it('should discover routes in nested router', () => {
const app = express();
const apiRouter = Router();
apiRouter.get('/users', (req, res) => res.json([]));
app.use('/api/v1', apiRouter);
const routes = discovery.discover(app);
expect(routes).toHaveLength(1);
expect(routes[0].path).toBe('/api/v1/users');
});
});
describe('Error Handling', () => {
it('should handle null app', () => {
const routes = discovery.discover(null as any);
expect(routes).toEqual([]);
});
it('should handle app without router', () => {
const routes = discovery.discover({} as any);
expect(routes).toEqual([]);
});
});
});// src/middleware/customMiddleware.test.ts
import { describe, it, expect, vi } from 'vitest';
import { customMiddleware } from './customMiddleware';
import type { Request, Response, NextFunction } from 'express';
describe('customMiddleware', () => {
const createMockRequest = (overrides = {}): Request => ({
method: 'GET',
path: '/test',
headers: {},
query: {},
body: {},
...overrides,
} as Request);
const createMockResponse = (): Response => ({
statusCode: 200,
json: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
} as unknown as Response);
it('should call next when enabled', () => {
const middleware = customMiddleware({ enabled: true });
const req = createMockRequest();
const res = createMockResponse();
const next: NextFunction = vi.fn();
middleware(req, res, next);
expect(next).toHaveBeenCalled();
});
it('should skip processing when disabled', () => {
const middleware = customMiddleware({ enabled: false });
const req = createMockRequest();
const res = createMockResponse();
const next: NextFunction = vi.fn();
middleware(req, res, next);
expect(next).toHaveBeenCalled();
});
});it('should extract parameters from path', () => {
// Arrange
const extractor = new PathParameterExtractor();
const path = '/users/:userId/posts/:postId';
// Act
const result = extractor.extractPathParameters(path);
// Assert
expect(result.parameters).toHaveLength(2);
expect(result.parameters[0].name).toBe('userId');
expect(result.parameters[1].name).toBe('postId');
});describe('Given a valid Express app', () => {
let app: Express;
beforeEach(() => {
app = express();
app.get('/users', handler);
});
describe('When discovering routes', () => {
let routes: RouteMetadata[];
beforeEach(() => {
const discovery = new RouteDiscovery();
routes = discovery.discover(app);
});
it('Then it should find the GET route', () => {
expect(routes).toHaveLength(1);
expect(routes[0].method).toBe('GET');
});
});
});describe('Type Inference', () => {
const testCases = [
{ param: 'userId', expected: 'integer' },
{ param: 'slug', expected: 'string' },
{ param: 'active', expected: 'boolean' },
{ param: 'page', expected: 'integer' },
];
testCases.forEach(({ param, expected }) => {
it(`should infer ${expected} type for :${param}`, () => {
const result = extractor.extractPathParameters(`/items/:${param}`);
expect(result.parameters[0].schema.type).toBe(expected);
});
});
});it('should handle async file parsing', async () => {
const parser = new JsDocParser({ cwd: __dirname });
const routes = await parser.parse();
expect(routes.length).toBeGreaterThan(0);
});describe('Error Handling', () => {
it('should throw on invalid input', () => {
expect(() => {
parser.parse(null as any);
}).toThrow('Invalid input');
});
it('should handle parse errors gracefully', () => {
const source = '/** invalid jsdoc';
const routes = parser.parseSource(source);
// Should not throw, returns empty array
expect(routes).toEqual([]);
});
});import { vi } from 'vitest';
describe('With mocked dependencies', () => {
it('should use mocked storage', () => {
const mockStorage = {
store: vi.fn(),
retrieve: vi.fn().mockReturnValue([]),
};
const capture = runtimeCapture({
snapshotStorage: mockStorage as any,
});
// ... test code
expect(mockStorage.store).toHaveBeenCalled();
});
});import request from 'supertest';
describe('API Endpoints', () => {
it('should return OpenAPI spec', async () => {
const app = createTestApp();
const response = await request(app)
.get('/openapi.json')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body.openapi).toBe('3.1.0');
});
});- One assertion per test when possible - Makes failures easier to diagnose
- Use meaningful test names - Should describe the expected behavior
- Test behavior, not implementation - Tests should survive refactoring
- Keep tests independent - Each test should work in isolation
- Use fixtures for complex data - Store test data in
test/fixtures/ - Clean up after tests - Use
afterEachto reset state