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
Markdown
Comprehensive testing framework and utilities for MCP Backend framework.
- π§ͺ 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
This module is automatically installed when using the MCP Backend Generator.
**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')
}
};
```
```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%'
};
```
```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');
});
});
});
```
```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);
});
});
});
```
```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()
}));
```
```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;
```
```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');
});
});
```
```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}` }
});
}
```
```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');
});
});
});
```
```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();
});
});
```
```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"
}
}
```
- 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)
- 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
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