Analysis and Documentation: - Add ANALISIS-ALINEACION-WORKSPACE-2025-12-08.md with comprehensive gap analysis - Document SIMCO v3.2 system with 20+ directives - Identify alignment gaps between orchestration and projects New SaaS Products Structure: - Create apps/products/pos-micro/ - Ultra basic POS (~100 MXN/month) - Target: Mexican informal market (street vendors, small stores) - Features: Offline-first PWA, WhatsApp bot, minimal DB (~10 tables) - Create apps/products/erp-basico/ - Austere ERP (~300-500 MXN/month) - Target: SMBs needing full ERP without complexity - Features: Inherits from erp-core, modular pricing SaaS Layer: - Create apps/saas/ structure (billing, portal, admin, onboarding) - Add README.md and CONTEXTO-SAAS.md documentation Vertical Alignment: - Verify HERENCIA-ERP-CORE.md exists in all verticals - Add HERENCIA-SPECS-CORE.md to verticals - Update orchestration inventories Updates: - Update WORKSPACE-STATUS.md with new products and analysis - Update suite inventories with new structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
20 KiB
20 KiB
PATRÓN: TESTING
Versión: 1.0.0 Fecha: 2025-12-08 Aplica a: Backend (NestJS), Frontend (React) Prioridad: RECOMENDADA
PROPÓSITO
Definir patrones estándar de testing para garantizar código de calidad.
1. TIPOS DE TESTS
| Tipo | Qué Testea | Herramienta | Cobertura Objetivo |
|---|---|---|---|
| Unit | Funciones/Clases aisladas | Jest | 70%+ |
| Integration | Módulos integrados | Jest + Supertest | 50%+ |
| E2E | Flujos completos | Jest + Supertest | Críticos |
| Component | Componentes React | React Testing Library | 60%+ |
2. BACKEND: TEST DE SERVICE
Template
// user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserService } from './user.service';
import { UserEntity } from '../entities/user.entity';
import { CreateUserDto } from '../dto/create-user.dto';
import { NotFoundException, ConflictException } from '@nestjs/common';
describe('UserService', () => {
let service: UserService;
let repository: jest.Mocked<Repository<UserEntity>>;
// Mock del repositorio
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
remove: jest.fn(),
count: jest.fn(),
};
// Fixtures
const mockUser: UserEntity = {
id: '550e8400-e29b-41d4-a716-446655440000',
email: 'test@example.com',
name: 'Test User',
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
};
const createUserDto: CreateUserDto = {
email: 'new@example.com',
name: 'New User',
password: 'SecurePass123!',
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(UserEntity),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UserService>(UserService);
repository = module.get(getRepositoryToken(UserEntity));
// Reset mocks
jest.clearAllMocks();
});
describe('findOne', () => {
it('should return user when found', async () => {
// Arrange
mockRepository.findOne.mockResolvedValue(mockUser);
// Act
const result = await service.findOne(mockUser.id);
// Assert
expect(result).toEqual(mockUser);
expect(mockRepository.findOne).toHaveBeenCalledWith({
where: { id: mockUser.id },
});
});
it('should throw NotFoundException when user not found', async () => {
// Arrange
mockRepository.findOne.mockResolvedValue(null);
// Act & Assert
await expect(service.findOne('non-existent-id'))
.rejects
.toThrow(NotFoundException);
});
});
describe('create', () => {
it('should create user successfully', async () => {
// Arrange
mockRepository.findOne.mockResolvedValue(null); // No existe
mockRepository.create.mockReturnValue(mockUser);
mockRepository.save.mockResolvedValue(mockUser);
// Act
const result = await service.create(createUserDto);
// Assert
expect(result).toEqual(mockUser);
expect(mockRepository.findOne).toHaveBeenCalled();
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
email: createUserDto.email,
name: createUserDto.name,
})
);
expect(mockRepository.save).toHaveBeenCalled();
});
it('should throw ConflictException when email exists', async () => {
// Arrange
mockRepository.findOne.mockResolvedValue(mockUser); // Ya existe
// Act & Assert
await expect(service.create(createUserDto))
.rejects
.toThrow(ConflictException);
expect(mockRepository.save).not.toHaveBeenCalled();
});
});
describe('findAll', () => {
it('should return array of users', async () => {
// Arrange
const users = [mockUser, { ...mockUser, id: '2' }];
mockRepository.find.mockResolvedValue(users);
// Act
const result = await service.findAll();
// Assert
expect(result).toHaveLength(2);
expect(mockRepository.find).toHaveBeenCalled();
});
it('should return empty array when no users', async () => {
// Arrange
mockRepository.find.mockResolvedValue([]);
// Act
const result = await service.findAll();
// Assert
expect(result).toHaveLength(0);
});
});
describe('update', () => {
it('should update user successfully', async () => {
// Arrange
const updateDto = { name: 'Updated Name' };
const updatedUser = { ...mockUser, ...updateDto };
mockRepository.findOne.mockResolvedValue(mockUser);
mockRepository.save.mockResolvedValue(updatedUser);
// Act
const result = await service.update(mockUser.id, updateDto);
// Assert
expect(result.name).toBe('Updated Name');
expect(mockRepository.save).toHaveBeenCalled();
});
it('should throw NotFoundException when user not found', async () => {
// Arrange
mockRepository.findOne.mockResolvedValue(null);
// Act & Assert
await expect(service.update('non-existent', { name: 'Test' }))
.rejects
.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should remove user successfully', async () => {
// Arrange
mockRepository.findOne.mockResolvedValue(mockUser);
mockRepository.remove.mockResolvedValue(mockUser);
// Act
await service.remove(mockUser.id);
// Assert
expect(mockRepository.remove).toHaveBeenCalledWith(mockUser);
});
});
});
3. BACKEND: TEST DE CONTROLLER
Template
// user.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from '../services/user.service';
import { CreateUserDto } from '../dto/create-user.dto';
import { UserEntity } from '../entities/user.entity';
import { NotFoundException } from '@nestjs/common';
describe('UserController', () => {
let controller: UserController;
let service: jest.Mocked<UserService>;
const mockService = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
const mockUser: UserEntity = {
id: '550e8400-e29b-41d4-a716-446655440000',
email: 'test@example.com',
name: 'Test User',
status: 'active',
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
{
provide: UserService,
useValue: mockService,
},
],
}).compile();
controller = module.get<UserController>(UserController);
service = module.get(UserService);
jest.clearAllMocks();
});
describe('findAll', () => {
it('should return array of users', async () => {
// Arrange
mockService.findAll.mockResolvedValue([mockUser]);
// Act
const result = await controller.findAll();
// Assert
expect(result).toHaveLength(1);
expect(service.findAll).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return user by id', async () => {
// Arrange
mockService.findOne.mockResolvedValue(mockUser);
// Act
const result = await controller.findOne(mockUser.id);
// Assert
expect(result).toEqual(mockUser);
expect(service.findOne).toHaveBeenCalledWith(mockUser.id);
});
it('should propagate NotFoundException', async () => {
// Arrange
mockService.findOne.mockRejectedValue(new NotFoundException());
// Act & Assert
await expect(controller.findOne('non-existent'))
.rejects
.toThrow(NotFoundException);
});
});
describe('create', () => {
it('should create and return user', async () => {
// Arrange
const createDto: CreateUserDto = {
email: 'new@example.com',
name: 'New User',
password: 'Pass123!',
};
mockService.create.mockResolvedValue(mockUser);
// Act
const result = await controller.create(createDto);
// Assert
expect(result).toEqual(mockUser);
expect(service.create).toHaveBeenCalledWith(createDto);
});
});
describe('update', () => {
it('should update and return user', async () => {
// Arrange
const updateDto = { name: 'Updated' };
const updated = { ...mockUser, ...updateDto };
mockService.update.mockResolvedValue(updated);
// Act
const result = await controller.update(mockUser.id, updateDto);
// Assert
expect(result.name).toBe('Updated');
expect(service.update).toHaveBeenCalledWith(mockUser.id, updateDto);
});
});
describe('remove', () => {
it('should remove user', async () => {
// Arrange
mockService.remove.mockResolvedValue(undefined);
// Act
await controller.remove(mockUser.id);
// Assert
expect(service.remove).toHaveBeenCalledWith(mockUser.id);
});
});
});
4. BACKEND: TEST E2E
Template
// user.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('UserController (e2e)', () => {
let app: INestApplication;
let createdUserId: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('/users (POST)', () => {
it('should create user', () => {
return request(app.getHttpServer())
.post('/users')
.send({
email: 'e2e@test.com',
name: 'E2E User',
password: 'SecurePass123!',
})
.expect(201)
.expect((res) => {
expect(res.body.id).toBeDefined();
expect(res.body.email).toBe('e2e@test.com');
createdUserId = res.body.id;
});
});
it('should reject invalid email', () => {
return request(app.getHttpServer())
.post('/users')
.send({
email: 'invalid-email',
name: 'Test',
password: 'Pass123!',
})
.expect(400);
});
it('should reject duplicate email', () => {
return request(app.getHttpServer())
.post('/users')
.send({
email: 'e2e@test.com', // Ya existe
name: 'Duplicate',
password: 'Pass123!',
})
.expect(409);
});
});
describe('/users (GET)', () => {
it('should return users list', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
});
describe('/users/:id (GET)', () => {
it('should return user by id', () => {
return request(app.getHttpServer())
.get(`/users/${createdUserId}`)
.expect(200)
.expect((res) => {
expect(res.body.id).toBe(createdUserId);
});
});
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/550e8400-e29b-41d4-a716-446655440000')
.expect(404);
});
});
describe('/users/:id (PUT)', () => {
it('should update user', () => {
return request(app.getHttpServer())
.put(`/users/${createdUserId}`)
.send({ name: 'Updated Name' })
.expect(200)
.expect((res) => {
expect(res.body.name).toBe('Updated Name');
});
});
});
describe('/users/:id (DELETE)', () => {
it('should delete user', () => {
return request(app.getHttpServer())
.delete(`/users/${createdUserId}`)
.expect(204);
});
});
});
5. FRONTEND: TEST DE COMPONENTE
Template con React Testing Library
// UserCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { UserCard } from './UserCard';
import { User } from '@/types/user.types';
describe('UserCard', () => {
const mockUser: User = {
id: '1',
email: 'test@example.com',
name: 'Test User',
status: 'active',
};
const mockOnEdit = jest.fn();
const mockOnDelete = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('should render user information', () => {
render(<UserCard user={mockUser} />);
expect(screen.getByText('Test User')).toBeInTheDocument();
expect(screen.getByText('test@example.com')).toBeInTheDocument();
});
it('should call onEdit when edit button clicked', () => {
render(
<UserCard
user={mockUser}
onEdit={mockOnEdit}
/>
);
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(mockOnEdit).toHaveBeenCalledWith(mockUser);
});
it('should call onDelete when delete button clicked', () => {
render(
<UserCard
user={mockUser}
onDelete={mockOnDelete}
/>
);
fireEvent.click(screen.getByRole('button', { name: /delete/i }));
expect(mockOnDelete).toHaveBeenCalledWith(mockUser.id);
});
it('should show loading state', () => {
render(<UserCard user={mockUser} isLoading />);
expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument();
});
it('should hide actions when not provided', () => {
render(<UserCard user={mockUser} />);
expect(screen.queryByRole('button', { name: /edit/i })).not.toBeInTheDocument();
});
});
6. FRONTEND: TEST DE HOOK
Template
// useUsers.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useUsers, useCreateUser } from './useUsers';
import { userService } from '@/services/user.service';
// Mock del servicio
jest.mock('@/services/user.service');
const mockUserService = userService as jest.Mocked<typeof userService>;
describe('useUsers', () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
beforeEach(() => {
queryClient.clear();
jest.clearAllMocks();
});
describe('useUsers', () => {
it('should fetch users', async () => {
// Arrange
const mockUsers = [
{ id: '1', name: 'User 1', email: 'u1@test.com' },
{ id: '2', name: 'User 2', email: 'u2@test.com' },
];
mockUserService.getAll.mockResolvedValue(mockUsers);
// Act
const { result } = renderHook(() => useUsers(), { wrapper });
// Assert - Initially loading
expect(result.current.isLoading).toBe(true);
// Wait for data
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toEqual(mockUsers);
});
it('should handle error', async () => {
// Arrange
mockUserService.getAll.mockRejectedValue(new Error('Network error'));
// Act
const { result } = renderHook(() => useUsers(), { wrapper });
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBeDefined();
});
});
describe('useCreateUser', () => {
it('should create user and invalidate cache', async () => {
// Arrange
const newUser = { id: '3', name: 'New', email: 'new@test.com' };
mockUserService.create.mockResolvedValue(newUser);
// Act
const { result } = renderHook(() => useCreateUser(), { wrapper });
await result.current.mutateAsync({
name: 'New',
email: 'new@test.com',
password: 'Pass123!',
});
// Assert
expect(mockUserService.create).toHaveBeenCalled();
});
});
});
7. CONVENCIONES DE NOMBRES
// Archivos de test
user.service.spec.ts // Unit test backend
user.controller.spec.ts // Unit test controller
user.e2e-spec.ts // E2E test
UserCard.test.tsx // Component test
useUsers.test.tsx // Hook test
// Describe blocks
describe('UserService', () => { ... });
describe('findOne', () => { ... });
// Test cases
it('should return user when found', () => { ... });
it('should throw NotFoundException when user not found', () => { ... });
8. CHECKLIST DE TESTING
Por Service:
[ ] Test de cada método público
[ ] Test de casos de éxito
[ ] Test de casos de error (NotFoundException, etc.)
[ ] Test de validaciones de negocio
Por Controller:
[ ] Test de cada endpoint
[ ] Test de status codes correctos
[ ] Test de propagación de errores
Por Componente:
[ ] Test de render correcto
[ ] Test de eventos (click, change)
[ ] Test de estados (loading, error)
[ ] Test de props opcionales
Por Hook:
[ ] Test de fetch exitoso
[ ] Test de manejo de error
[ ] Test de mutaciones
Cobertura:
[ ] npm run test:cov muestra 70%+ en services
[ ] npm run test:cov muestra 60%+ en components
[ ] Tests críticos e2e pasan
9. COMANDOS
# Backend
npm run test # Unit tests
npm run test:watch # Watch mode
npm run test:cov # Con cobertura
npm run test:e2e # E2E tests
# Frontend
npm run test # All tests
npm run test -- --watch # Watch mode
npm run test -- --coverage # Con cobertura
npm run test -- UserCard # Test específico
Versión: 1.0.0 | Sistema: SIMCO | Tipo: Patrón de Testing