# 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 ```typescript // 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>; // 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); 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 ```typescript // 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; 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); 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 ```typescript // 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 ```typescript // 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(); expect(screen.getByText('Test User')).toBeInTheDocument(); expect(screen.getByText('test@example.com')).toBeInTheDocument(); }); it('should call onEdit when edit button clicked', () => { render( ); fireEvent.click(screen.getByRole('button', { name: /edit/i })); expect(mockOnEdit).toHaveBeenCalledWith(mockUser); }); it('should call onDelete when delete button clicked', () => { render( ); fireEvent.click(screen.getByRole('button', { name: /delete/i })); expect(mockOnDelete).toHaveBeenCalledWith(mockUser.id); }); it('should show loading state', () => { render(); expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument(); }); it('should hide actions when not provided', () => { render(); expect(screen.queryByRole('button', { name: /edit/i })).not.toBeInTheDocument(); }); }); ``` --- ## 6. FRONTEND: TEST DE HOOK ### Template ```typescript // 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; describe('useUsers', () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); const wrapper = ({ children }) => ( {children} ); 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 ```typescript // 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 ```bash # 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