UNPKG

backend-mcp

Version:

Generador automΓ‘tico de backends con Node.js, Express, Prisma y mΓ³dulos configurables. Servidor MCP compatible con npx para agentes IA. Soporta PostgreSQL, MySQL, MongoDB y SQLite.

983 lines (804 loc) β€’ 28.2 kB
# Testing Module Comprehensive testing framework and utilities for MCP Backend framework. ## Features - πŸ§ͺ Unit testing with Jest - πŸ”— Integration testing - 🌐 End-to-end (E2E) testing - πŸ“Š Code coverage reporting - 🎭 Test mocking and stubbing - πŸ”„ Test data factories - πŸ› Snapshot testing - ⚑ Performance testing - πŸ”’ Security testing - πŸ“ˆ Load testing ## Installation This module is automatically installed when using the MCP Backend Generator. ## Configuration ### Environment Variables **General Configuration:** - `NODE_ENV` - Set to 'test' for testing environment - `TEST_TIMEOUT` (optional) - Test timeout in milliseconds (default: 30000) - `TEST_PARALLEL` (optional) - Run tests in parallel (default: true) - `TEST_COVERAGE` (optional) - Enable coverage reporting (default: true) - `TEST_VERBOSE` (optional) - Verbose test output (default: false) **Database Testing:** - `TEST_DATABASE_URL` - Test database connection string - `TEST_DB_RESET` (optional) - Reset database before tests (default: true) - `TEST_DB_SEED` (optional) - Seed test data (default: true) **API Testing:** - `TEST_API_BASE_URL` (optional) - Base URL for API tests (default: http://localhost:3000) - `TEST_API_TIMEOUT` (optional) - API request timeout (default: 10000) - `TEST_API_RETRIES` (optional) - Number of retries for failed requests (default: 3) **Performance Testing:** - `TEST_LOAD_USERS` (optional) - Number of virtual users for load testing (default: 10) - `TEST_LOAD_DURATION` (optional) - Load test duration in seconds (default: 60) - `TEST_LOAD_RAMP_UP` (optional) - Ramp-up time in seconds (default: 10) ### Configuration File ```typescript // src/config/testing.ts export const testingConfig = { timeout: parseInt(process.env.TEST_TIMEOUT || '30000'), parallel: process.env.TEST_PARALLEL !== 'false', coverage: process.env.TEST_COVERAGE !== 'false', verbose: process.env.TEST_VERBOSE === 'true', database: { url: process.env.TEST_DATABASE_URL, reset: process.env.TEST_DB_RESET !== 'false', seed: process.env.TEST_DB_SEED !== 'false' }, api: { baseUrl: process.env.TEST_API_BASE_URL || 'http://localhost:3000', timeout: parseInt(process.env.TEST_API_TIMEOUT || '10000'), retries: parseInt(process.env.TEST_API_RETRIES || '3') }, performance: { users: parseInt(process.env.TEST_LOAD_USERS || '10'), duration: parseInt(process.env.TEST_LOAD_DURATION || '60'), rampUp: parseInt(process.env.TEST_LOAD_RAMP_UP || '10') } }; ``` ### Jest Configuration ```javascript // jest.config.js module.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src', '<rootDir>/tests'], testMatch: [ '**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts' ], transform: { '^.+\.ts$': 'ts-jest' }, collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/index.ts', '!src/config/**' ], coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } }, setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'], testTimeout: 30000, maxWorkers: '50%' }; ``` ## Usage ### Unit Testing ```typescript // tests/unit/userService.test.ts import { userService } from '../../src/services/userService'; import { userRepository } from '../../src/repositories/userRepository'; import { UserFactory } from '../factories/userFactory'; // Mock dependencies jest.mock('../../src/repositories/userRepository'); const mockUserRepository = userRepository as jest.Mocked<typeof userRepository>; describe('UserService', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('createUser', () => { it('should create a new user successfully', async () => { // Arrange const userData = UserFactory.build(); const expectedUser = { ...userData, id: 1, createdAt: new Date() }; mockUserRepository.create.mockResolvedValue(expectedUser); // Act const result = await userService.createUser(userData); // Assert expect(result).toEqual(expectedUser); expect(mockUserRepository.create).toHaveBeenCalledWith(userData); expect(mockUserRepository.create).toHaveBeenCalledTimes(1); }); it('should throw error when email already exists', async () => { // Arrange const userData = UserFactory.build({ email: 'existing@example.com' }); const error = new Error('Email already exists'); mockUserRepository.create.mockRejectedValue(error); // Act & Assert await expect(userService.createUser(userData)) .rejects.toThrow('Email already exists'); }); it('should validate user data before creation', async () => { // Arrange const invalidUserData = { email: 'invalid-email' }; // Act & Assert await expect(userService.createUser(invalidUserData)) .rejects.toThrow('Invalid email format'); expect(mockUserRepository.create).not.toHaveBeenCalled(); }); }); describe('getUserById', () => { it('should return user when found', async () => { // Arrange const userId = 1; const expectedUser = UserFactory.build({ id: userId }); mockUserRepository.findById.mockResolvedValue(expectedUser); // Act const result = await userService.getUserById(userId); // Assert expect(result).toEqual(expectedUser); expect(mockUserRepository.findById).toHaveBeenCalledWith(userId); }); it('should throw error when user not found', async () => { // Arrange const userId = 999; mockUserRepository.findById.mockResolvedValue(null); // Act & Assert await expect(userService.getUserById(userId)) .rejects.toThrow('User not found'); }); }); }); ``` ### Integration Testing ```typescript // tests/integration/userAPI.test.ts import request from 'supertest'; import { app } from '../../src/app'; import { database } from '../../src/database'; import { UserFactory } from '../factories/userFactory'; import { testHelper } from '../helpers/testHelper'; describe('User API Integration Tests', () => { beforeAll(async () => { await database.connect(); await testHelper.setupDatabase(); }); afterAll(async () => { await testHelper.cleanupDatabase(); await database.disconnect(); }); beforeEach(async () => { await testHelper.resetDatabase(); }); describe('POST /api/users', () => { it('should create a new user', async () => { // Arrange const userData = UserFactory.build(); // Act const response = await request(app) .post('/api/users') .send(userData) .expect(201); // Assert expect(response.body).toMatchObject({ id: expect.any(Number), email: userData.email, name: userData.name, createdAt: expect.any(String) }); expect(response.body.password).toBeUndefined(); // Verify in database const userInDb = await database.user.findUnique({ where: { id: response.body.id } }); expect(userInDb).toBeTruthy(); expect(userInDb.email).toBe(userData.email); }); it('should return 400 for invalid data', async () => { // Arrange const invalidUserData = { email: 'invalid-email', name: '', password: '123' // Too short }; // Act const response = await request(app) .post('/api/users') .send(invalidUserData) .expect(400); // Assert expect(response.body.errors).toEqual( expect.arrayContaining([ expect.objectContaining({ field: 'email', message: expect.stringContaining('valid email') }), expect.objectContaining({ field: 'name', message: expect.stringContaining('required') }), expect.objectContaining({ field: 'password', message: expect.stringContaining('minimum') }) ]) ); }); it('should return 409 for duplicate email', async () => { // Arrange const existingUser = await testHelper.createUser(); const duplicateUserData = UserFactory.build({ email: existingUser.email }); // Act const response = await request(app) .post('/api/users') .send(duplicateUserData) .expect(409); // Assert expect(response.body.error).toContain('Email already exists'); }); }); describe('GET /api/users/:id', () => { it('should return user by id', async () => { // Arrange const user = await testHelper.createUser(); // Act const response = await request(app) .get(`/api/users/${user.id}`) .expect(200); // Assert expect(response.body).toMatchObject({ id: user.id, email: user.email, name: user.name }); expect(response.body.password).toBeUndefined(); }); it('should return 404 for non-existent user', async () => { // Act const response = await request(app) .get('/api/users/999') .expect(404); // Assert expect(response.body.error).toContain('User not found'); }); }); describe('PUT /api/users/:id', () => { it('should update user with authentication', async () => { // Arrange const user = await testHelper.createUser(); const token = await testHelper.generateAuthToken(user); const updateData = { name: 'Updated Name' }; // Act const response = await request(app) .put(`/api/users/${user.id}`) .set('Authorization', `Bearer ${token}`) .send(updateData) .expect(200); // Assert expect(response.body.name).toBe(updateData.name); // Verify in database const updatedUser = await database.user.findUnique({ where: { id: user.id } }); expect(updatedUser.name).toBe(updateData.name); }); it('should return 401 without authentication', async () => { // Arrange const user = await testHelper.createUser(); const updateData = { name: 'Updated Name' }; // Act await request(app) .put(`/api/users/${user.id}`) .send(updateData) .expect(401); }); }); }); ``` ### Test Factories ```typescript // tests/factories/userFactory.ts import { Factory } from 'fishery'; import { faker } from '@faker-js/faker'; import { User } from '../../src/types/user'; export const UserFactory = Factory.define<User>(({ sequence }) => ({ id: sequence, email: faker.internet.email(), name: faker.person.fullName(), password: faker.internet.password({ length: 12 }), role: 'user', isActive: true, createdAt: faker.date.past(), updatedAt: faker.date.recent() })); // Specialized factories export const AdminUserFactory = UserFactory.params({ role: 'admin', permissions: ['read', 'write', 'delete'] }); export const InactiveUserFactory = UserFactory.params({ isActive: false }); // tests/factories/productFactory.ts export const ProductFactory = Factory.define<Product>(({ sequence }) => ({ id: sequence, name: faker.commerce.productName(), description: faker.commerce.productDescription(), price: parseFloat(faker.commerce.price()), sku: faker.string.alphanumeric(8).toUpperCase(), category: faker.commerce.department(), inStock: faker.datatype.boolean(), quantity: faker.number.int({ min: 0, max: 100 }), createdAt: faker.date.past(), updatedAt: faker.date.recent() })); // tests/factories/orderFactory.ts export const OrderFactory = Factory.define<Order>(({ sequence, associations }) => ({ id: sequence, userId: associations.user?.id || UserFactory.build().id, status: faker.helpers.arrayElement(['pending', 'processing', 'shipped', 'delivered']), total: parseFloat(faker.commerce.price({ min: 10, max: 1000 })), items: ProductFactory.buildList(faker.number.int({ min: 1, max: 5 })), shippingAddress: { street: faker.location.streetAddress(), city: faker.location.city(), state: faker.location.state(), zipCode: faker.location.zipCode(), country: faker.location.country() }, createdAt: faker.date.past(), updatedAt: faker.date.recent() })); ``` ### Test Helpers ```typescript // tests/helpers/testHelper.ts import { database } from '../../src/database'; import { UserFactory } from '../factories/userFactory'; import { authService } from '../../src/services/authService'; import { redis } from '../../src/cache'; export class TestHelper { static async setupDatabase() { // Run migrations await database.migrate.latest(); } static async cleanupDatabase() { // Clean up test data await database.raw('TRUNCATE TABLE users, products, orders CASCADE'); } static async resetDatabase() { // Reset database state between tests await this.cleanupDatabase(); await this.seedTestData(); } static async seedTestData() { // Create basic test data const adminUser = await this.createUser({ email: 'admin@test.com', role: 'admin' }); const regularUser = await this.createUser({ email: 'user@test.com', role: 'user' }); return { adminUser, regularUser }; } static async createUser(overrides = {}) { const userData = UserFactory.build(overrides); return await database.user.create({ data: userData }); } static async createProduct(overrides = {}) { const productData = ProductFactory.build(overrides); return await database.product.create({ data: productData }); } static async createOrder(userId, overrides = {}) { const orderData = OrderFactory.build({ userId, ...overrides }); return await database.order.create({ data: orderData }); } static async generateAuthToken(user) { return await authService.generateToken(user); } static async clearCache() { await redis.flushall(); } static async waitFor(condition, timeout = 5000) { const start = Date.now(); while (Date.now() - start < timeout) { if (await condition()) { return true; } await new Promise(resolve => setTimeout(resolve, 100)); } throw new Error(`Condition not met within ${timeout}ms`); } static mockDate(date = new Date()) { const mockDate = jest.spyOn(global, 'Date').mockImplementation(() => date); return () => mockDate.mockRestore(); } static mockEnvironment(env = {}) { const originalEnv = process.env; process.env = { ...originalEnv, ...env }; return () => { process.env = originalEnv; }; } } export const testHelper = TestHelper; ``` ### End-to-End Testing ```typescript // tests/e2e/userJourney.test.ts import { chromium, Browser, Page } from 'playwright'; import { testHelper } from '../helpers/testHelper'; describe('User Journey E2E Tests', () => { let browser: Browser; let page: Page; beforeAll(async () => { browser = await chromium.launch({ headless: true }); }); afterAll(async () => { await browser.close(); }); beforeEach(async () => { page = await browser.newPage(); await testHelper.resetDatabase(); }); afterEach(async () => { await page.close(); }); it('should complete user registration and login flow', async () => { // Navigate to registration page await page.goto('http://localhost:3000/register'); // Fill registration form await page.fill('[data-testid="email"]', 'test@example.com'); await page.fill('[data-testid="name"]', 'Test User'); await page.fill('[data-testid="password"]', 'SecurePassword123'); await page.fill('[data-testid="confirmPassword"]', 'SecurePassword123'); // Submit registration await page.click('[data-testid="register-button"]'); // Verify registration success await page.waitForSelector('[data-testid="registration-success"]'); expect(await page.textContent('[data-testid="registration-success"]')) .toContain('Registration successful'); // Navigate to login page await page.goto('http://localhost:3000/login'); // Fill login form await page.fill('[data-testid="email"]', 'test@example.com'); await page.fill('[data-testid="password"]', 'SecurePassword123'); // Submit login await page.click('[data-testid="login-button"]'); // Verify login success await page.waitForSelector('[data-testid="dashboard"]'); expect(await page.textContent('[data-testid="user-name"]')) .toContain('Test User'); }); it('should handle product purchase flow', async () => { // Setup test data const user = await testHelper.createUser(); const product = await testHelper.createProduct({ price: 29.99 }); // Login as user await page.goto('http://localhost:3000/login'); await page.fill('[data-testid="email"]', user.email); await page.fill('[data-testid="password"]', 'password'); await page.click('[data-testid="login-button"]'); // Navigate to product page await page.goto(`http://localhost:3000/products/${product.id}`); // Add to cart await page.click('[data-testid="add-to-cart"]'); // Verify cart update await page.waitForSelector('[data-testid="cart-count"]'); expect(await page.textContent('[data-testid="cart-count"]')).toBe('1'); // Go to checkout await page.click('[data-testid="cart-icon"]'); await page.click('[data-testid="checkout-button"]'); // Fill shipping information await page.fill('[data-testid="shipping-address"]', '123 Test St'); await page.fill('[data-testid="shipping-city"]', 'Test City'); await page.fill('[data-testid="shipping-zip"]', '12345'); // Fill payment information await page.fill('[data-testid="card-number"]', '4111111111111111'); await page.fill('[data-testid="card-expiry"]', '12/25'); await page.fill('[data-testid="card-cvv"]', '123'); // Submit order await page.click('[data-testid="place-order"]'); // Verify order confirmation await page.waitForSelector('[data-testid="order-confirmation"]'); expect(await page.textContent('[data-testid="order-total"]')) .toContain('$29.99'); }); }); ``` ### Performance Testing ```typescript // tests/performance/loadTest.test.ts import { check, sleep } from 'k6'; import http from 'k6/http'; import { Rate } from 'k6/metrics'; export let errorRate = new Rate('errors'); export let options = { stages: [ { duration: '10s', target: 10 }, // Ramp up { duration: '30s', target: 50 }, // Stay at 50 users { duration: '10s', target: 0 }, // Ramp down ], thresholds: { http_req_duration: ['p(95)<500'], // 95% of requests under 500ms errors: ['rate<0.1'], // Error rate under 10% }, }; const BASE_URL = 'http://localhost:3000'; export function setup() { // Create test user const response = http.post(`${BASE_URL}/api/auth/register`, { email: 'loadtest@example.com', password: 'LoadTest123', name: 'Load Test User' }); return { authToken: response.json('token') }; } export default function(data) { const headers = { 'Authorization': `Bearer ${data.authToken}`, 'Content-Type': 'application/json' }; // Test user profile endpoint let response = http.get(`${BASE_URL}/api/users/me`, { headers }); check(response, { 'profile status is 200': (r) => r.status === 200, 'profile response time < 200ms': (r) => r.timings.duration < 200, }) || errorRate.add(1); sleep(1); // Test product listing endpoint response = http.get(`${BASE_URL}/api/products?page=1&limit=10`); check(response, { 'products status is 200': (r) => r.status === 200, 'products response time < 300ms': (r) => r.timings.duration < 300, 'products count > 0': (r) => r.json('data').length > 0, }) || errorRate.add(1); sleep(1); // Test search endpoint response = http.get(`${BASE_URL}/api/products/search?q=test`); check(response, { 'search status is 200': (r) => r.status === 200, 'search response time < 400ms': (r) => r.timings.duration < 400, }) || errorRate.add(1); sleep(2); } export function teardown(data) { // Cleanup test data http.del(`${BASE_URL}/api/users/me`, { headers: { 'Authorization': `Bearer ${data.authToken}` } }); } ``` ### Security Testing ```typescript // tests/security/security.test.ts import request from 'supertest'; import { app } from '../../src/app'; import { testHelper } from '../helpers/testHelper'; describe('Security Tests', () => { beforeEach(async () => { await testHelper.resetDatabase(); }); describe('SQL Injection Protection', () => { it('should prevent SQL injection in user search', async () => { const maliciousQuery = "'; DROP TABLE users; --"; const response = await request(app) .get(`/api/users/search?q=${encodeURIComponent(maliciousQuery)}`) .expect(400); expect(response.body.error).toContain('Invalid search query'); // Verify users table still exists const users = await request(app) .get('/api/users') .expect(200); expect(Array.isArray(users.body.data)).toBe(true); }); }); describe('XSS Protection', () => { it('should sanitize HTML in user input', async () => { const maliciousInput = { name: '<script>alert("XSS")</script>John Doe', email: 'john@example.com', password: 'SecurePassword123' }; const response = await request(app) .post('/api/users') .send(maliciousInput) .expect(201); expect(response.body.name).toBe('John Doe'); expect(response.body.name).not.toContain('<script>'); }); }); describe('Authentication Security', () => { it('should prevent brute force attacks', async () => { const user = await testHelper.createUser({ email: 'test@example.com', password: 'correct-password' }); // Attempt multiple failed logins for (let i = 0; i < 6; i++) { await request(app) .post('/api/auth/login') .send({ email: user.email, password: 'wrong-password' }); } // Next attempt should be rate limited const response = await request(app) .post('/api/auth/login') .send({ email: user.email, password: 'correct-password' }) .expect(429); expect(response.body.error).toContain('Too many attempts'); }); it('should require strong passwords', async () => { const weakPasswords = [ '123456', 'password', 'qwerty', '12345678', 'abc123' ]; for (const password of weakPasswords) { const response = await request(app) .post('/api/users') .send({ name: 'Test User', email: `test${Date.now()}@example.com`, password }) .expect(400); expect(response.body.errors).toEqual( expect.arrayContaining([ expect.objectContaining({ field: 'password', message: expect.stringContaining('strong') }) ]) ); } }); }); describe('Authorization Security', () => { it('should prevent unauthorized access to admin endpoints', async () => { const regularUser = await testHelper.createUser({ role: 'user' }); const token = await testHelper.generateAuthToken(regularUser); const response = await request(app) .get('/api/admin/users') .set('Authorization', `Bearer ${token}`) .expect(403); expect(response.body.error).toContain('Insufficient permissions'); }); it('should prevent users from accessing other users\' data', async () => { const user1 = await testHelper.createUser(); const user2 = await testHelper.createUser(); const token1 = await testHelper.generateAuthToken(user1); const response = await request(app) .get(`/api/users/${user2.id}`) .set('Authorization', `Bearer ${token1}`) .expect(403); expect(response.body.error).toContain('Access denied'); }); }); describe('Input Validation Security', () => { it('should validate file uploads', async () => { const user = await testHelper.createUser(); const token = await testHelper.generateAuthToken(user); // Test malicious file upload const response = await request(app) .post('/api/users/avatar') .set('Authorization', `Bearer ${token}`) .attach('avatar', Buffer.from('<?php system($_GET["cmd"]); ?>'), 'malicious.php') .expect(400); expect(response.body.error).toContain('Invalid file type'); }); }); }); ``` ### Snapshot Testing ```typescript // tests/snapshots/apiResponses.test.ts import request from 'supertest'; import { app } from '../../src/app'; import { testHelper } from '../helpers/testHelper'; describe('API Response Snapshots', () => { beforeEach(async () => { await testHelper.resetDatabase(); }); it('should match user list response structure', async () => { // Create test users await testHelper.createUser({ name: 'John Doe', email: 'john@example.com' }); await testHelper.createUser({ name: 'Jane Smith', email: 'jane@example.com' }); const response = await request(app) .get('/api/users') .expect(200); // Remove dynamic fields for consistent snapshots const sanitizedResponse = { ...response.body, data: response.body.data.map(user => ({ ...user, id: '[ID]', createdAt: '[DATE]', updatedAt: '[DATE]' })) }; expect(sanitizedResponse).toMatchSnapshot(); }); it('should match error response structure', async () => { const response = await request(app) .post('/api/users') .send({ email: 'invalid-email', name: '', password: '123' }) .expect(400); expect(response.body).toMatchSnapshot(); }); }); ``` ## Testing Scripts ```json // package.json scripts { "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "test:unit": "jest tests/unit", "test:integration": "jest tests/integration", "test:e2e": "playwright test", "test:security": "jest tests/security", "test:performance": "k6 run tests/performance/loadTest.js", "test:ci": "jest --ci --coverage --watchAll=false", "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand" } } ``` ## Dependencies - jest (Testing framework) - @types/jest (TypeScript support) - ts-jest (TypeScript preprocessor) - supertest (HTTP testing) - playwright (E2E testing) - k6 (Performance testing) - fishery (Test data factories) - @faker-js/faker (Fake data generation) - jest-extended (Additional matchers) - nock (HTTP mocking) ## Integration - Integrates with all modules for comprehensive testing - Provides test utilities and helpers - Works with CI/CD pipelines - Supports database testing with transactions - Integrates with code coverage tools ## Best Practices 1. **Test Structure**: Follow AAA pattern (Arrange, Act, Assert) 2. **Test Isolation**: Each test should be independent 3. **Test Data**: Use factories for consistent test data 4. **Mocking**: Mock external dependencies appropriately 5. **Coverage**: Aim for high code coverage with meaningful tests 6. **Performance**: Test critical performance scenarios 7. **Security**: Include security-focused tests 8. **Documentation**: Document complex test scenarios ## License MIT