@jamx-framework/testing
v1.0.0
Published
JAMX Framework — Testing utilities
Downloads
265
Maintainers
Readme
@jamx-framework/testing
Descripción
Utilidades de testing para JAMX Framework. Proporciona herramientas para tests de integración y unitarios, incluyendo un cliente HTTP, un servidor HTTP minimalista, factories de objetos, y mocks de los servicios principales de JAMX (mailer, storage, cache, queue).
Cómo funciona
El paquete ofrece tres categorías de herramientas:
- TestClient: Cliente HTTP para realizar requests a servidores reales (ideal para tests de integración)
- TestServer: Servidor HTTP minimalista que corre en memoria para simular endpoints
- Factories: Funciones para crear objetos de test con datos predeterminados y overrides
- Mocks: Implementaciones en memoria de servicios JAMX para aislar tests
Componentes principales
TestClient (src/test-client.ts)
Cliente HTTP que realiza requests reales a un servidor:
get(path, options): Realiza petición GETpost(path, options): Realiza petición POST con body JSONput(path, options): Realiza petición PUTpatch(path, options): Realiza petición PATCHdelete(path, options): Realiza petición DELETE
Retorna TestResponse con métodos json<T>() y text().
TestServer (src/test-server.ts)
Servidor HTTP minimalista para tests:
get/post/put/delete(path, handler): Define rutasuse(handler): Middleware globalstart(): Inicia el servidor en puerto aleatoriostop(): Detiene el servidorurl: URL base del servidor (ej:http://localhost:3000)
Los handlers reciben (req, res) donde req tiene body y params parseados.
Factories (src/factories.ts)
createFactory<T>(defaults): Crea una factory de objetoscreateSequence<T>(fn): Crea un generador de valores secuenciales
Mocks
mockMailer(): Mock del servicio de emailmockStorage(baseUrl?): Mock del storage (S3/local)mockCache(prefix?): Mock del cache (Redis/memory)mockQueue<T>(handler?): Mock de cola de jobs
Uso básico
TestClient
import { TestClient } from '@jamx-framework/testing';
const client = new TestClient('http://localhost:3000');
// GET request
const response = await client.get('/api/users');
const users = response.json<User[]>();
// POST request
const createResponse = await client.post('/api/users', {
body: { name: 'Alice', email: '[email protected]' },
headers: { Authorization: 'Bearer token' },
});
const newUser = createResponse.json<User>();TestServer
import { createTestServer } from '@jamx-framework/testing';
const server = createTestServer();
// Definir rutas
server.get('/api/users', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify([{ id: 1, name: 'Alice' }]));
});
server.post('/api/users', async (req, res) => {
const user = req.body as { name: string };
// Guardar en DB...
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ...user, id: 1 }));
});
// Iniciar servidor
const baseUrl = await server.start();
console.log(`Server running at ${baseUrl}`);
// Hacer requests con TestClient
const client = new TestClient(baseUrl);
const response = await client.get('/api/users');
// Detener servidor
await server.stop();TestServer con middleware
server.use(async (req, res) => {
// Middleware de logging
console.log(`${req.method} ${req.url}`);
// Agregar header
res.setHeader('X-Custom-Header', 'value');
});
server.get('/api/data', (req, res) => {
// El middleware ya ejecutó
res.writeHead(200);
res.end('OK');
});TestServer con params de ruta
server.get('/api/users/:id', (req, res) => {
const id = req.params?.id; // Extraído automáticamente
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id, name: 'User' }));
});
// GET /api/users/123 → req.params = { id: '123' }Factories
import { createFactory, createSequence } from '@jamx-framework/testing';
// Factory de usuarios
const makeUser = createFactory({
id: '1',
name: 'Test User',
email: '[email protected]',
role: 'user',
});
const user1 = makeUser(); // { id: '1', name: 'Test User', ... }
const admin = makeUser({ role: 'admin', name: 'Admin' }); // overrides
// Secuencia de IDs
const nextId = createSequence((n) => String(n));
const id1 = nextId(); // '1'
const id2 = nextId(); // '2'
// Secuencia de emails
const nextEmail = createSequence((n) => `user${n}@test.com`);
const email1 = nextEmail(); // '[email protected]'Mocks
mockMailer
import { mockMailer } from '@jamx-framework/testing';
const mailer = mockMailer();
// Enviar email
await mailer.send({
to: '[email protected]',
subject: 'Welcome',
html: '<h1>Hello</h1>',
});
// Enviar con template
await mailer.sendTemplate('welcome', {
to: '[email protected]',
data: { name: 'Alice' },
});
// Verificar emails enviados
expect(mailer.sent).toHaveLength(2);
expect(mailer.sent[0].to).toBe('[email protected]');
expect(mailer.lastSent()?.subject).toBe('Welcome');
// Limpiar
mailer.clear();mockStorage
import { mockStorage } from '@jamx-framework/testing';
const storage = mockStorage('http://localhost/files');
// Subir archivo
await storage.put('avatar.png', Buffer.from('image data'), {
contentType: 'image/png',
});
// Descargar archivo
const data = await storage.get('avatar.png');
expect(data?.toString()).toBe('image data');
// Verificar existencia
expect(await storage.exists('avatar.png')).toBe(true);
// Listar archivos
const files = await storage.list();
expect(files).toHaveLength(1);
// URL pública (para tests)
const url = await storage.url('avatar.png');
expect(url).toBe('http://localhost/files/avatar.png');
// Limpiar
storage.clear();mockCache
import { mockCache } from '@jamx-framework/testing';
const cache = mockCache('test:');
// Guardar
await cache.set('key1', { value: 123 }, 60); // TTL 60s
// Obtener
const data = await cache.get('key1');
expect(data?.value).toBe(123);
// Verificar existencia
expect(await cache.has('key1')).toBe(true);
// Eliminar
await cache.delete('key1');
// Limpiar todo
cache.clear();mockQueue
import { mockQueue } from '@jamx-framework/testing';
// Mock sin handler (solo registro)
const queue = mockQueue<{ userId: number }>();
// Añadir jobs
await queue.add('send-email', { userId: 1 });
await queue.add('send-email', { userId: 2 });
// Ver jobs encolados
expect(queue.jobs).toHaveLength(2);
expect(queue.jobs[0].name).toBe('send-email');
// Con handler para procesar
const queueWithHandler = mockQueue<{ userId: number }>(async (job) => {
console.log(`Processing job ${job.id} for user ${job.data.userId}`);
});
await queueWithHandler.add('notify', { userId: 1 });
await queueWithHandler.processAll(); // Procesa todos los jobs
// Limpiar
queue.clear();Ejemplo completo de test de integración
import { createTestServer, TestClient, createFactory } from '@jamx-framework/testing';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
const makeUser = createFactory({
id: 1,
name: 'Test User',
email: '[email protected]',
});
describe('User API', () => {
let server: ReturnType<typeof createTestServer>;
let client: TestClient;
beforeEach(async () => {
server = createTestServer();
// Setup routes
server.get('/api/users', (req, res) => {
const users = [
makeUser({ id: 1, name: 'Alice' }),
makeUser({ id: 2, name: 'Bob' }),
];
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
});
server.post('/api/users', async (req, res) => {
const user = req.body as { name: string; email: string };
const newUser = makeUser({
id: Date.now(),
name: user.name,
email: user.email,
});
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(newUser));
});
const baseUrl = await server.start();
client = new TestClient(baseUrl);
});
afterEach(async () => {
await server.stop();
});
it('should list all users', async () => {
const response = await client.get('/api/users');
expect(response.status).toBe(200);
const users = response.json<Array<{ id: number; name: string }>>();
expect(users).toHaveLength(2);
expect(users[0].name).toBe('Alice');
});
it('should create a new user', async () => {
const response = await client.post('/api/users', {
body: { name: 'Charlie', email: '[email protected]' },
});
expect(response.status).toBe(201);
const user = response.json<{ id: number; name: string; email: string }>();
expect(user.name).toBe('Charlie');
expect(user.email).toBe('[email protected]');
});
});API Reference
TestClient
Constructor
new TestClient(baseUrl: string)Crea un cliente HTTP que apunta a baseUrl.
Métodos
get<T = unknown>(path: string, options?: RequestOptions): Promise<TestResponse>
post<T = unknown>(path: string, options?: RequestOptions): Promise<TestResponse>
put<T = unknown>(path: string, options?: RequestOptions): Promise<TestResponse>
patch<T = unknown>(path: string, options?: RequestOptions): Promise<TestResponse>
delete<T = unknown>(path: string, options?: RequestOptions): Promise<TestResponse>TestResponse
interface TestResponse {
status: number;
headers: Record<string, string>;
body: string;
json<T = unknown>(): T;
text(): string;
}RequestOptions
interface RequestOptions {
headers?: Record<string, string>;
body?: unknown; // Se serializa como JSON automáticamente
}TestServer
Interface
interface TestServer {
get(path: string, handler: TestHandler): this;
post(path: string, handler: TestHandler): this;
put(path: string, handler: TestHandler): this;
delete(path: string, handler: TestHandler): this;
use(handler: TestHandler): this;
start(): Promise<string>; // retorna baseUrl
stop(): Promise<void>;
url: string;
}TestHandler
type TestHandler = (
req: http.IncomingMessage & {
body?: unknown;
params?: Record<string, string>;
},
res: http.ServerResponse,
) => void | Promise<void>;Factories
createFactory
function createFactory<T extends object>(defaults: T): (overrides?: DeepPartial<T>) => TCrea una función que retorna objetos con valores por defecto, fusionando overrides.
createSequence
function createSequence<T>(fn: (n: number) => T): () => TCrea un generador que retorna valores secuenciales únicos.
Mocks
mockMailer
function mockMailer(): MockMailerMockMailer
interface MockMailer {
send(msg: MockMailMessage): Promise<void>;
sendTemplate(template: string, options: { to: string; data?: Record<string, unknown> }): Promise<void>;
sent: MockMailMessage[];
clear(): void;
hasSent(to: string): boolean;
lastSent(): MockMailMessage | null;
}mockStorage
function mockStorage(baseUrl?: string): MockStorageMockStorage
interface MockStorage {
put(key: string, data: Buffer, options?: { contentType?: string }): Promise<void>;
get(key: string): Promise<Buffer | null>;
delete(key: string): Promise<boolean>;
exists(key: string): Promise<boolean>;
url(key: string): Promise<string>;
list(prefix?: string): Promise<MockStorageFile[]>;
files: Map<string, MockStorageFile>;
clear(): void;
}mockCache
function mockCache(prefix?: string): CacheRetorna una instancia de Cache con driver memory.
mockQueue
function mockQueue<T = unknown>(handler?: MockJobHandler<T>): MockQueue<T>MockQueue
interface MockQueue<T = unknown> {
add(name: string, data: T): Promise<MockJob<T>>;
processAll(): Promise<void>;
jobs: MockJob<T>[];
clear(): void;
}Configuración
tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/.tsbuildinfo"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}Scripts disponibles
pnpm build- Compila TypeScript a JavaScriptpnpm dev- Compilación en watch modepnpm test- Ejecuta tests unitariospnpm test:watch- Tests en watch modepnpm type-check- Verifica tipos sin compilarpnpm clean- Limpia archivos compilados
Testing
Tests en packages/testing/tests/unit/:
factories.test.ts: Tests de createFactory y createSequencemocks.test.ts: Tests de todos los mockstest-server.test.ts: Tests de TestServer
Ejecutar tests:
pnpm testDependencias
@jamx-framework/cache- Para mockCache@jamx-framework/core- Para tipos base@jamx-framework/server- Para TestServer@jamx-framework/validator- Para validación (opcional)@types/node- Tipos de Node.jsvitest- Framework de testing
Casos de uso
Tests de integración con API real
// Usar TestClient para probar un servidor JAMX real
const client = new TestClient('http://localhost:3000');
test('should create user', async () => {
const res = await client.post('/users', {
body: { name: 'Alice', email: '[email protected]' },
});
expect(res.status).toBe(201);
const user = res.json<User>();
expect(user.id).toBeDefined();
});Tests de endpoints aislados
// Usar TestServer para probar lógica de endpoints sin levantar servidor completo
const server = createTestServer();
server.get('/health', (req, res) => {
res.writeHead(200);
res.end(JSON.stringify({ status: 'ok' }));
});
await server.start();
const client = new TestClient(server.url);
const res = await client.get('/health');
expect(res.json()).toEqual({ status: 'ok' });
await server.stop();Tests con datos de ejemplo
const makeProduct = createFactory({
id: 0,
name: 'Product',
price: 0,
inStock: true,
});
test('should filter products', () => {
const products = [
makeProduct({ id: 1, name: 'Apple', price: 1.5 }),
makeProduct({ id: 2, name: 'Banana', price: 0.8 }),
makeProduct({ id: 3, name: 'Carrot', price: 0.5 }),
];
const cheap = products.filter(p => p.price < 1);
expect(cheap).toHaveLength(2);
});Tests de servicios con mocks
import { mockMailer, mockStorage } from '@jamx-framework/testing';
test('should send welcome email on signup', async () => {
const mailer = mockMailer();
const user = { id: 1, email: '[email protected]', name: 'Alice' };
// Lógica de signup que usa mailer
await mailer.send({
to: user.email,
subject: 'Welcome!',
html: `<h1>Hello ${user.name}</h1>`,
});
expect(mailer.sent).toHaveLength(1);
expect(mailer.sent[0].to).toBe(user.email);
});
test('should upload file to storage', async () => {
const storage = mockStorage();
const fileData = Buffer.from('file content');
await storage.put('documents/file.txt', fileData, {
contentType: 'text/plain',
});
expect(await storage.exists('documents/file.txt')).toBe(true);
const retrieved = await storage.get('documents/file.txt');
expect(retrieved?.toString()).toBe('file content');
});Limitaciones
- TestServer: No soporta HTTPS (solo HTTP)
- TestServer: No tiene soporte para WebSocket
- TestClient: Bloqueante en Node.js (pero usa async/await)
- Mocks: Todos en memoria, no persisten entre tests
- mockQueue: Procesa jobs secuencialmente, no en paralelo
Buenas prácticas
- Siempre limpiar mocks:
afterEach(() => {
mailer.clear();
storage.clear();
cache.clear();
queue.clear();
});- Usar factories para datos de test:
const makeUser = createFactory({ id: 1, name: 'User', email: '[email protected]' });
const user = makeUser({ name: 'Alice' }); // más legible- Asegurar puertos libres:
// TestServer ya usa puerto 0 (aleatorio), no hay conflicto- Mockear dependencias externas:
// En lugar de llamar a API real, usar mockStorage
const storage = mockStorage();
await storage.put('key', data);- Tests asincrónicos:
// TestServer.start() y stop() son async
beforeEach(async () => {
await server.start();
});
afterEach(async () => {
await server.stop();
});Integración con otros paquetes
@jamx-framework/server: TestServer puede simular endpoints de JAMX server@jamx-framework/cache: mockCache retorna instancia de Cache real@jamx-framework/storage: mockStorage implementa StorageAdapter@jamx-framework/queue: mockQueue simula Queue@jamx-framework/mailer: mockMailer simula Mailer
Ejemplo: Test completo de API JAMX
import { createTestServer } from '@jamx-framework/testing';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
// Simular una API JAMX completa
const server = createTestServer();
server.use((req, res) => {
// Middleware de logging
console.log(`${req.method} ${req.url}`);
next();
});
server.get('/api/health', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));
});
server.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body as { email: string; password: string };
if (email === '[email protected]' && password === 'pass') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ token: 'jwt-token', user: { id: 1, email } }));
} else {
res.writeHead(401);
res.end(JSON.stringify({ error: 'Invalid credentials' }));
}
});
describe('API Integration Tests', () => {
let client: TestClient;
beforeEach(async () => {
await server.start();
client = new TestClient(server.url);
});
afterEach(async () => {
await server.stop();
});
it('health check', async () => {
const res = await client.get('/api/health');
expect(res.status).toBe(200);
expect(res.json()).toHaveProperty('status', 'ok');
});
it('login success', async () => {
const res = await client.post('/api/auth/login', {
body: { email: '[email protected]', password: 'pass' },
});
expect(res.status).toBe(200);
const data = res.json<{ token: string; user: { id: number; email: string } }>();
expect(data.token).toBe('jwt-token');
});
it('login failure', async () => {
const res = await client.post('/api/auth/login', {
body: { email: '[email protected]', password: 'wrong' },
});
expect(res.status).toBe(401);
});
});