Testing
Fox Framework incluye un sistema integral de testing que facilita la creación de pruebas unitarias, de integración y end-to-end para tus aplicaciones. Diseñado para trabajar perfectamente con Jest, Vitest y otros frameworks de testing populares.
CaracterÃsticas del Sistema de Testing
- Testing unitario: Pruebas aisladas de componentes individuales
- Testing de integración: Pruebas de interacción entre componentes
- Testing end-to-end: Pruebas completas del flujo de la aplicación
- Mocking integrado: Sistema de mocks para dependencias externas
- Test fixtures: Datos de prueba reutilizables
- Coverage reporting: Reportes de cobertura de código
- CI/CD integration: Integración con pipelines de integración continua
Configuración Inicial
Instalación de Dependencias
npm install --save-dev @foxframework/testing jest @types/jest supertest @types/supertestConfiguración de Jest
jest.config.js
module.exports = {
preset: '@foxframework/testing',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: [
'**/__tests__/**/*.ts',
'**/?(*.)+(spec|test).ts'
],
transform: {
'^.+\\.ts$': 'ts-jest'
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
'!src/**/index.ts'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts']
};Configuración del Entorno de Testing
tests/setup.ts
import { TestingModule } from '@foxframework/testing';
import { DatabaseTestHelper } from '@foxframework/testing/database';
// Configuración global para tests
beforeAll(async () => {
await TestingModule.initialize({
database: {
type: 'sqlite',
database: ':memory:',
synchronize: true
},
cache: {
type: 'memory'
}
});
});
afterAll(async () => {
await TestingModule.cleanup();
});
beforeEach(async () => {
await DatabaseTestHelper.clearDatabase();
});Testing de Controladores
Test Unitario de Controlador
import { TestingModule, MockRequest, MockResponse } from '@foxframework/testing';
import { UserController } from '../src/controllers/user.controller';
import { UserService } from '../src/services/user.service';
describe('UserController', () => {
let controller: UserController;
let userService: jest.Mocked<UserService>;
let req: MockRequest;
let res: MockResponse;
beforeEach(() => {
// Crear mocks de dependencias
userService = TestingModule.createMock<UserService>({
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn()
});
// Crear instancia del controlador con dependencias mockeadas
controller = new UserController(userService);
// Crear objetos request y response mockeados
req = TestingModule.createMockRequest();
res = TestingModule.createMockResponse();
});
describe('getUserById', () => {
it('should return user when found', async () => {
// Arrange
const userId = '123';
const expectedUser = { id: userId, name: 'John Doe', email: 'john@example.com' };
req.params = { id: userId };
userService.findById.mockResolvedValue(expectedUser);
// Act
await controller.getUserById(req, res);
// Assert
expect(userService.findById).toHaveBeenCalledWith(userId);
expect(res.json).toHaveBeenCalledWith({
success: true,
data: expectedUser
});
expect(res.status).toHaveBeenCalledWith(200);
});
it('should return 404 when user not found', async () => {
// Arrange
const userId = '999';
req.params = { id: userId };
userService.findById.mockResolvedValue(null);
// Act
await controller.getUserById(req, res);
// Assert
expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({
success: false,
message: 'User not found'
});
});
it('should handle service errors', async () => {
// Arrange
const userId = '123';
const error = new Error('Database connection failed');
req.params = { id: userId };
userService.findById.mockRejectedValue(error);
// Act & Assert
await expect(controller.getUserById(req, res)).rejects.toThrow(error);
});
});
});Test de Integración de Controlador
import { TestingModule, TestServer } from '@foxframework/testing';
import request from 'supertest';
import { Application } from 'express';
describe('User API Integration', () => {
let app: Application;
let server: TestServer;
beforeAll(async () => {
server = await TestingModule.createTestServer({
controllers: ['src/controllers/**/*.controller.ts'],
services: ['src/services/**/*.service.ts']
});
app = server.getApplication();
});
afterAll(async () => {
await server.close();
});
describe('GET /api/users/:id', () => {
it('should return user data', async () => {
// Crear usuario de prueba
const user = await server.createTestUser({
name: 'John Doe',
email: 'john@example.com'
});
const response = await request(app)
.get(`/api/users/${user.id}`)
.expect(200);
expect(response.body).toMatchObject({
success: true,
data: {
id: user.id,
name: 'John Doe',
email: 'john@example.com'
}
});
});
});
});Testing de Servicios
Test Unitario de Servicio
import { TestingModule } from '@foxframework/testing';
import { UserService } from '../src/services/user.service';
import { UserRepository } from '../src/repositories/user.repository';
import { EmailService } from '../src/services/email.service';
describe('UserService', () => {
let service: UserService;
let userRepository: jest.Mocked<UserRepository>;
let emailService: jest.Mocked<EmailService>;
beforeEach(() => {
userRepository = TestingModule.createMock<UserRepository>({
findById: jest.fn(),
save: jest.fn(),
delete: jest.fn()
});
emailService = TestingModule.createMock<EmailService>({
sendWelcomeEmail: jest.fn()
});
service = new UserService(userRepository, emailService);
});
describe('createUser', () => {
it('should create user and send welcome email', async () => {
// Arrange
const userData = { name: 'John Doe', email: 'john@example.com' };
const createdUser = { id: '1', ...userData };
userRepository.save.mockResolvedValue(createdUser);
emailService.sendWelcomeEmail.mockResolvedValue(undefined);
// Act
const result = await service.createUser(userData);
// Assert
expect(userRepository.save).toHaveBeenCalledWith(userData);
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(createdUser);
expect(result).toEqual(createdUser);
});
it('should rollback user creation if email fails', async () => {
// Arrange
const userData = { name: 'John Doe', email: 'john@example.com' };
const createdUser = { id: '1', ...userData };
userRepository.save.mockResolvedValue(createdUser);
emailService.sendWelcomeEmail.mockRejectedValue(new Error('Email service down'));
userRepository.delete.mockResolvedValue(undefined);
// Act & Assert
await expect(service.createUser(userData)).rejects.toThrow('Email service down');
expect(userRepository.delete).toHaveBeenCalledWith(createdUser.id);
});
});
});Testing de Middleware
Test de Middleware de Autenticación
import { TestingModule, MockRequest, MockResponse, MockNextFunction } from '@foxframework/testing';
import { authMiddleware } from '../src/middleware/auth.middleware';
import { JwtService } from '../src/services/jwt.service';
describe('AuthMiddleware', () => {
let jwtService: jest.Mocked<JwtService>;
let req: MockRequest;
let res: MockResponse;
let next: MockNextFunction;
beforeEach(() => {
jwtService = TestingModule.createMock<JwtService>({
verify: jest.fn(),
decode: jest.fn()
});
req = TestingModule.createMockRequest();
res = TestingModule.createMockResponse();
next = TestingModule.createMockNext();
});
it('should allow access with valid token', async () => {
// Arrange
const token = 'valid-jwt-token';
const user = { id: '1', email: 'user@example.com' };
req.headers = { authorization: `Bearer ${token}` };
jwtService.verify.mockResolvedValue(user);
// Act
await authMiddleware(jwtService)(req, res, next);
// Assert
expect(jwtService.verify).toHaveBeenCalledWith(token);
expect(req.user).toEqual(user);
expect(next).toHaveBeenCalledWith();
});
it('should reject request without token', async () => {
// Arrange
req.headers = {};
// Act
await authMiddleware(jwtService)(req, res, next);
// Assert
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
success: false,
message: 'No token provided'
});
expect(next).not.toHaveBeenCalled();
});
});Testing de Base de Datos
Test de Repository
import { TestingModule, DatabaseTestHelper } from '@foxframework/testing';
import { UserRepository } from '../src/repositories/user.repository';
import { User } from '../src/entities/user.entity';
describe('UserRepository', () => {
let repository: UserRepository;
beforeAll(async () => {
await TestingModule.initializeDatabase();
repository = TestingModule.getRepository(UserRepository);
});
beforeEach(async () => {
await DatabaseTestHelper.clearTable('users');
});
describe('findByEmail', () => {
it('should find user by email', async () => {
// Arrange
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'hashedpassword'
};
await DatabaseTestHelper.insertUser(userData);
// Act
const user = await repository.findByEmail('john@example.com');
// Assert
expect(user).toBeDefined();
expect(user.email).toBe('john@example.com');
expect(user.name).toBe('John Doe');
});
it('should return null for non-existent email', async () => {
// Act
const user = await repository.findByEmail('nonexistent@example.com');
// Assert
expect(user).toBeNull();
});
});
});Testing End-to-End
Configuración E2E
import { TestingModule, E2ETestHelper } from '@foxframework/testing';
import { Application } from 'express';
describe('User Management E2E', () => {
let app: Application;
let testHelper: E2ETestHelper;
beforeAll(async () => {
app = await TestingModule.createE2EApp();
testHelper = new E2ETestHelper(app);
});
beforeEach(async () => {
await testHelper.seedDatabase();
});
afterEach(async () => {
await testHelper.cleanDatabase();
});
it('should complete full user registration flow', async () => {
// 1. Registrar usuario
const registrationData = {
name: 'John Doe',
email: 'john@example.com',
password: 'securepassword'
};
const registerResponse = await testHelper
.request()
.post('/api/auth/register')
.send(registrationData)
.expect(201);
expect(registerResponse.body.success).toBe(true);
const userId = registerResponse.body.data.id;
// 2. Verificar email
const verificationToken = await testHelper.getEmailVerificationToken(userId);
await testHelper
.request()
.get(`/api/auth/verify-email?token=${verificationToken}`)
.expect(200);
// 3. Login
const loginResponse = await testHelper
.request()
.post('/api/auth/login')
.send({
email: 'john@example.com',
password: 'securepassword'
})
.expect(200);
const authToken = loginResponse.body.data.token;
// 4. Acceder a recurso protegido
await testHelper
.request()
.get('/api/users/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
});
});Test Fixtures y Factories
Test Factories
import { Factory } from '@foxframework/testing';
import { User } from '../src/entities/user.entity';
import { Post } from '../src/entities/post.entity';
// Factory para usuarios
export const UserFactory = Factory.define<User>(User, (faker) => ({
name: faker.name.fullName(),
email: faker.internet.email(),
password: faker.internet.password(),
createdAt: faker.date.past(),
updatedAt: faker.date.recent()
}));
// Factory para posts
export const PostFactory = Factory.define<Post>(Post, (faker) => ({
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(3),
published: faker.datatype.boolean(),
authorId: () => UserFactory.create().then(user => user.id),
createdAt: faker.date.past(),
updatedAt: faker.date.recent()
}));
// Usar en tests
describe('Post Service', () => {
it('should create post with author', async () => {
// Crear usuario y post usando factories
const author = await UserFactory.create();
const post = await PostFactory.create({ authorId: author.id });
expect(post.authorId).toBe(author.id);
});
});Test Fixtures
export const testFixtures = {
users: {
admin: {
name: 'Admin User',
email: 'admin@example.com',
role: 'admin',
permissions: ['read', 'write', 'delete']
},
regular: {
name: 'Regular User',
email: 'user@example.com',
role: 'user',
permissions: ['read']
}
},
posts: {
published: {
title: 'Published Post',
content: 'This is a published post',
published: true
},
draft: {
title: 'Draft Post',
content: 'This is a draft post',
published: false
}
}
};Testing de Performance
Benchmark Tests
import { PerformanceTestHelper } from '@foxframework/testing';
describe('API Performance', () => {
let helper: PerformanceTestHelper;
beforeAll(async () => {
helper = new PerformanceTestHelper();
await helper.initialize();
});
it('should handle concurrent users', async () => {
const result = await helper.loadTest({
url: '/api/users',
method: 'GET',
concurrency: 100,
duration: 30000, // 30 segundos
rampUp: 5000 // 5 segundos para alcanzar concurrencia máxima
});
expect(result.averageResponseTime).toBeLessThan(100); // ms
expect(result.errorRate).toBeLessThan(0.01); // 1%
expect(result.throughput).toBeGreaterThan(1000); // requests/second
});
it('should maintain performance under stress', async () => {
const result = await helper.stressTest({
url: '/api/users',
method: 'POST',
payload: { name: 'Test User', email: 'test@example.com' },
maxConcurrency: 500,
duration: 60000
});
expect(result.breakingPoint).toBeGreaterThan(300);
});
});Comandos de Testing
Scripts de NPM
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --testMatch='**/*.unit.test.ts'",
"test:integration": "jest --testMatch='**/*.integration.test.ts'",
"test:e2e": "jest --testMatch='**/*.e2e.test.ts'",
"test:performance": "jest --testMatch='**/*.performance.test.ts'"
}
}Ejecución de Tests
# Ejecutar todos los tests
npm test
# Ejecutar tests con watch mode
npm run test:watch
# Ejecutar tests especÃficos
npm test -- --testNamePattern="UserController"
# Ejecutar tests de un archivo especÃfico
npm test -- src/controllers/user.controller.test.ts
# Generar reporte de cobertura
npm run test:coverage
# Ejecutar solo tests unitarios
npm run test:unitMejores Prácticas
1. Estructura de Tests
- Organiza tests en carpetas que reflejen la estructura del código
- Usa naming descriptivo para tests
- Agrupa tests relacionados con
describe
2. Arranging Tests (AAA Pattern)
it('should do something', () => {
// Arrange - Preparar datos y mocks
const input = 'test data';
const expected = 'expected result';
// Act - Ejecutar la función bajo test
const result = functionUnderTest(input);
// Assert - Verificar el resultado
expect(result).toBe(expected);
});3. Mocking Strategy
- Mock dependencias externas (APIs, base de datos)
- Usa mocks especÃficos para cada test
- Verifica que los mocks sean llamados correctamente
4. Test Data Management
- Usa factories para generar datos de test
- Limpia la base de datos entre tests
- Usa fixtures para datos estáticos
5. Performance
- Ejecuta tests unitarios frecuentemente
- Ejecuta tests de integración en CI/CD
- Monitorea el tiempo de ejecución de tests
El sistema de testing de Fox Framework te proporciona todas las herramientas necesarias para mantener la calidad de tu código y detectar problemas temprano en el desarrollo.