← Volver al listado de tecnologías
Testing Avanzado: MSW, Testing Library y Mejores Prácticas
Capítulo 6.5: Testing Avanzado: MSW, Testing Library y Mejores Prácticas
En este capítulo especial profundizaremos en técnicas avanzadas de testing, configuración completa de MSW y patterns profesionales para testing en React Native.
🎯 Objetivos del Capítulo
- Configuración avanzada de Mock Service Worker (MSW)
- Testing Library patterns para componentes complejos
- Testing de integración entre componentes y stores
- Mocking profesional y fixtures
- Performance testing y testing de accesibilidad
📦 Configuración Avanzada de MSW
Instalación Completa
# MSW y polyfills
npm install --save-dev msw @mswjs/react-native-polyfills
# Utilidades adicionales para testing
npm install --save-dev @testing-library/user-event
npm install --save-dev jest-environment-jsdom
# Para testing de componentes con animaciones
npm install --save-dev react-native-testing-mocks
Configuración Global de MSW
// src/test/mocks/setup.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
// Configurar servidor MSW
export const server = setupServer(...handlers);
// Setup global para Jest
export const setupMSWGlobal = () => {
// Configurar polyfills para React Native
require('@mswjs/react-native-polyfills');
beforeAll(() => {
// Iniciar servidor
server.listen({
onUnhandledRequest: 'warn',
});
});
beforeEach(() => {
// Limpiar mocks antes de cada test
jest.clearAllMocks();
});
afterEach(() => {
// Reset handlers después de cada test
server.resetHandlers();
});
afterAll(() => {
// Cerrar servidor
server.close();
});
};
Handlers Avanzados con Escenarios
// src/test/mocks/handlers/todoHandlers.ts
import { rest } from 'msw';
import { Todo, Category } from '@/types/store';
// Datos mock más realistas
const mockTodos: Todo[] = [
{
id: '1',
title: 'Completar proyecto React Native',
description: 'Finalizar la aplicación TODO con todas las funcionalidades',
completed: false,
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
userId: 'user-123',
categoryId: 'work',
priority: 'high',
dueDate: new Date('2024-12-31'),
tags: ['react-native', 'proyecto', 'urgente'],
},
{
id: '2',
title: 'Hacer compras del supermercado',
description: 'Leche, pan, huevos, frutas',
completed: true,
createdAt: new Date('2024-01-02'),
updatedAt: new Date('2024-01-03'),
userId: 'user-123',
categoryId: 'personal',
priority: 'medium',
dueDate: new Date('2024-01-05'),
tags: ['compras', 'hogar'],
},
];
const mockCategories: Category[] = [
{
id: 'work',
name: 'Trabajo',
color: '#3B82F6',
icon: 'briefcase',
userId: 'user-123',
},
{
id: 'personal',
name: 'Personal',
color: '#10B981',
icon: 'home',
userId: 'user-123',
},
];
// Helper para simular delays de red
const networkDelay = () => Math.random() * 1000 + 500;
export const todoHandlers = [
// GET /todos - Con filtros y paginación
rest.get('/api/todos', async (req, res, ctx) => {
const url = req.url;
const userId = url.searchParams.get('userId');
const category = url.searchParams.get('category');
const completed = url.searchParams.get('completed');
const search = url.searchParams.get('search');
const page = parseInt(url.searchParams.get('page') || '1');
const limit = parseInt(url.searchParams.get('limit') || '10');
// Simular delay de red
await ctx.delay(networkDelay());
let filteredTodos = mockTodos.filter(todo =>
!userId || todo.userId === userId
);
// Aplicar filtros
if (category) {
filteredTodos = filteredTodos.filter(todo => todo.categoryId === category);
}
if (completed !== null) {
const isCompleted = completed === 'true';
filteredTodos = filteredTodos.filter(todo => todo.completed === isCompleted);
}
if (search) {
const searchLower = search.toLowerCase();
filteredTodos = filteredTodos.filter(todo =>
todo.title.toLowerCase().includes(searchLower) ||
todo.description.toLowerCase().includes(searchLower) ||
todo.tags.some(tag => tag.toLowerCase().includes(searchLower))
);
}
// Paginación
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedTodos = filteredTodos.slice(startIndex, endIndex);
return res(
ctx.status(200),
ctx.json({
todos: paginatedTodos,
pagination: {
page,
limit,
total: filteredTodos.length,
totalPages: Math.ceil(filteredTodos.length / limit),
},
})
);
}),
// POST /todos - Con validación
rest.post('/api/todos', async (req, res, ctx) => {
const todoData = await req.json();
// Simular validación
if (!todoData.title || todoData.title.length < 3) {
return res(
ctx.status(400),
ctx.json({
error: 'Title must be at least 3 characters long',
field: 'title',
})
);
}
await ctx.delay(networkDelay());
const newTodo: Todo = {
...todoData,
id: `todo-${Date.now()}`,
createdAt: new Date(),
updatedAt: new Date(),
};
mockTodos.push(newTodo);
return res(
ctx.status(201),
ctx.json({ todo: newTodo })
);
}),
// PUT /todos/:id - Con optimistic updates
rest.put('/api/todos/:id', async (req, res, ctx) => {
const { id } = req.params;
const updates = await req.json();
await ctx.delay(networkDelay());
const todoIndex = mockTodos.findIndex(todo => todo.id === id);
if (todoIndex === -1) {
return res(
ctx.status(404),
ctx.json({ error: 'Todo not found' })
);
}
mockTodos[todoIndex] = {
...mockTodos[todoIndex],
...updates,
updatedAt: new Date(),
};
return res(
ctx.status(200),
ctx.json({ todo: mockTodos[todoIndex] })
);
}),
// DELETE /todos/:id
rest.delete('/api/todos/:id', async (req, res, ctx) => {
const { id } = req.params;
await ctx.delay(networkDelay());
const todoIndex = mockTodos.findIndex(todo => todo.id === id);
if (todoIndex === -1) {
return res(
ctx.status(404),
ctx.json({ error: 'Todo not found' })
);
}
mockTodos.splice(todoIndex, 1);
return res(ctx.status(204));
}),
// GET /categories
rest.get('/api/categories', async (req, res, ctx) => {
const userId = req.url.searchParams.get('userId');
await ctx.delay(networkDelay());
const filteredCategories = mockCategories.filter(category =>
!userId || category.userId === userId
);
return res(
ctx.status(200),
ctx.json({ categories: filteredCategories })
);
}),
];
// Handlers para escenarios de error
export const errorHandlers = [
// Simular error de servidor
rest.get('/api/todos', (req, res, ctx) => {
return res(
ctx.status(500),
ctx.json({ error: 'Internal server error' })
);
}),
// Simular error de red
rest.post('/api/todos', (req, res, ctx) => {
return res.networkError('Network connection failed');
}),
// Simular timeout
rest.put('/api/todos/:id', async (req, res, ctx) => {
await ctx.delay(10000); // 10 segundos
return res(ctx.status(408), ctx.json({ error: 'Request timeout' }));
}),
];
Configuración de Escenarios de Testing
// src/test/mocks/scenarios.ts
import { server } from './setup';
import { errorHandlers, todoHandlers } from './handlers/todoHandlers';
export const testScenarios = {
// Escenario normal
normal: () => {
server.use(...todoHandlers);
},
// Escenario de errores
serverError: () => {
server.use(...errorHandlers);
},
// Escenario de datos vacíos
emptyData: () => {
server.use(
rest.get('/api/todos', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
todos: [],
pagination: { page: 1, limit: 10, total: 0, totalPages: 0 },
})
);
})
);
},
// Escenario de carga lenta
slowNetwork: () => {
server.use(
rest.get('/api/todos', async (req, res, ctx) => {
await ctx.delay(5000);
return res(
ctx.status(200),
ctx.json({
todos: [],
pagination: { page: 1, limit: 10, total: 0, totalPages: 0 },
})
);
})
);
},
};
🧪 Testing Avanzado de Componentes
Testing de Componentes con Estados Complejos
// src/components/__tests__/TodoList.test.tsx
import React from 'react';
import { render, fireEvent, waitFor, within } from '@testing-library/react-native';
import { TodoList } from '../TodoList';
import { renderWithProviders } from '@/test/utils';
import { testScenarios } from '@/test/mocks/scenarios';
import { setupMSWGlobal } from '@/test/mocks/setup';
// Setup MSW
setupMSWGlobal();
describe('TodoList Component', () => {
beforeEach(() => {
testScenarios.normal();
});
describe('Loading States', () => {
it('should show loading indicator while fetching todos', async () => {
testScenarios.slowNetwork();
const { getByTestId } = renderWithProviders(<TodoList />);
expect(getByTestId('loading-indicator')).toBeTruthy();
await waitFor(
() => {
expect(() => getByTestId('loading-indicator')).toThrow();
},
{ timeout: 6000 }
);
});
it('should show skeleton loading for better UX', async () => {
testScenarios.slowNetwork();
const { getByTestId } = renderWithProviders(<TodoList />);
expect(getByTestId('skeleton-loader')).toBeTruthy();
});
});
describe('Error States', () => {
it('should show error message when API fails', async () => {
testScenarios.serverError();
const { getByText, getByTestId } = renderWithProviders(<TodoList />);
await waitFor(() => {
expect(getByText('Error al cargar las tareas')).toBeTruthy();
});
expect(getByTestId('retry-button')).toBeTruthy();
});
it('should retry loading when retry button is pressed', async () => {
testScenarios.serverError();
const { getByTestId } = renderWithProviders(<TodoList />);
await waitFor(() => {
expect(getByTestId('retry-button')).toBeTruthy();
});
// Cambiar a escenario normal
testScenarios.normal();
fireEvent.press(getByTestId('retry-button'));
await waitFor(() => {
expect(getByTestId('todo-list')).toBeTruthy();
});
});
});
describe('Empty States', () => {
it('should show empty state when no todos exist', async () => {
testScenarios.emptyData();
const { getByText, getByTestId } = renderWithProviders(<TodoList />);
await waitFor(() => {
expect(getByText('No tienes tareas pendientes')).toBeTruthy();
});
expect(getByTestId('add-first-todo-button')).toBeTruthy();
});
it('should show filtered empty state', async () => {
const { getByTestId, getByText } = renderWithProviders(
<TodoList filter={{ completed: true }} />
);
await waitFor(() => {
expect(getByText('No hay tareas completadas')).toBeTruthy();
});
});
});
describe('Interaction Testing', () => {
it('should toggle todo completion', async () => {
const { getByTestId } = renderWithProviders(<TodoList />);
await waitFor(() => {
expect(getByTestId('todo-item-1')).toBeTruthy();
});
const todoItem = getByTestId('todo-item-1');
const checkbox = within(todoItem).getByTestId('todo-checkbox');
fireEvent.press(checkbox);
await waitFor(() => {
expect(within(todoItem).getByTestId('completed-todo')).toBeTruthy();
});
});
it('should navigate to todo detail on press', async () => {
const mockNavigate = jest.fn();
jest.mock('@/hooks/useTypedNavigation', () => ({
useTodoNavigation: () => ({
navigateToTodoDetail: mockNavigate,
}),
}));
const { getByTestId } = renderWithProviders(<TodoList />);
await waitFor(() => {
expect(getByTestId('todo-item-1')).toBeTruthy();
});
fireEvent.press(getByTestId('todo-item-1'));
expect(mockNavigate).toHaveBeenCalledWith('1');
});
});
describe('Accessibility Testing', () => {
it('should have proper accessibility labels', async () => {
const { getByTestId } = renderWithProviders(<TodoList />);
await waitFor(() => {
expect(getByTestId('todo-item-1')).toBeTruthy();
});
const todoItem = getByTestId('todo-item-1');
expect(todoItem.props.accessibilityLabel).toContain('Tarea: Completar proyecto React Native');
expect(todoItem.props.accessibilityHint).toBe('Toca para ver detalles');
});
it('should support screen reader navigation', async () => {
const { getByTestId } = renderWithProviders(<TodoList />);
await waitFor(() => {
expect(getByTestId('todo-list')).toBeTruthy();
});
const todoList = getByTestId('todo-list');
expect(todoList.props.accessibilityRole).toBe('list');
});
});
describe('Performance Testing', () => {
it('should handle large lists efficiently', async () => {
// Mock una lista grande
const largeTodoList = Array.from({ length: 1000 }, (_, i) => ({
id: `todo-${i}`,
title: `Tarea ${i}`,
description: `Descripción ${i}`,
completed: i % 3 === 0,
createdAt: new Date(),
updatedAt: new Date(),
userId: 'user-123',
categoryId: 'work',
priority: 'medium',
dueDate: null,
tags: [],
}));
server.use(
rest.get('/api/todos', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
todos: largeTodoList,
pagination: { page: 1, limit: 1000, total: 1000, totalPages: 1 },
})
);
})
);
const startTime = performance.now();
const { getByTestId } = renderWithProviders(<TodoList />);
await waitFor(() => {
expect(getByTestId('todo-list')).toBeTruthy();
});
const endTime = performance.now();
const renderTime = endTime - startTime;
// El render no debería tomar más de 2 segundos
expect(renderTime).toBeLessThan(2000);
});
});
});
Testing de Hooks Personalizados
// src/hooks/__tests__/useTodoOperations.test.ts
import { renderHook, act, waitFor } from '@testing-library/react-native';
import { useTodoOperations } from '../useTodoOperations';
import { setupMSWGlobal } from '@/test/mocks/setup';
import { testScenarios } from '@/test/mocks/scenarios';
import { createWrapper } from '@/test/utils';
setupMSWGlobal();
describe('useTodoOperations Hook', () => {
const wrapper = createWrapper();
beforeEach(() => {
testScenarios.normal();
});
describe('Creating Todos', () => {
it('should create a new todo successfully', async () => {
const { result } = renderHook(() => useTodoOperations(), { wrapper });
const newTodo = {
title: 'Nueva tarea',
description: 'Descripción de la nueva tarea',
completed: false,
userId: 'user-123',
categoryId: 'work',
priority: 'medium' as const,
dueDate: null,
tags: ['nueva'],
};
await act(async () => {
await result.current.createTodo(newTodo);
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});
it('should handle validation errors', async () => {
const { result } = renderHook(() => useTodoOperations(), { wrapper });
const invalidTodo = {
title: 'AB', // Muy corto
description: '',
completed: false,
userId: 'user-123',
categoryId: 'work',
priority: 'medium' as const,
dueDate: null,
tags: [],
};
await act(async () => {
await result.current.createTodo(invalidTodo);
});
expect(result.current.error).toContain('Title must be at least 3 characters');
});
});
describe('Updating Todos', () => {
it('should update todo optimistically', async () => {
const { result } = renderHook(() => useTodoOperations(), { wrapper });
// Primero cargar todos
await act(async () => {
await result.current.loadTodos();
});
const todoId = '1';
const updates = { title: 'Título actualizado' };
await act(async () => {
await result.current.updateTodo(todoId, updates);
});
// Verificar que la actualización fue optimista
expect(result.current.todos.find(t => t.id === todoId)?.title).toBe('Título actualizado');
});
it('should revert optimistic update on error', async () => {
testScenarios.serverError();
const { result } = renderHook(() => useTodoOperations(), { wrapper });
// Cargar datos iniciales con escenario normal
testScenarios.normal();
await act(async () => {
await result.current.loadTodos();
});
const originalTitle = result.current.todos.find(t => t.id === '1')?.title;
// Cambiar a escenario de error
testScenarios.serverError();
const updates = { title: 'Título que fallará' };
await act(async () => {
await result.current.updateTodo('1', updates);
});
// Verificar que se revirtió la actualización
expect(result.current.todos.find(t => t.id === '1')?.title).toBe(originalTitle);
expect(result.current.error).toBeTruthy();
});
});
describe('Batch Operations', () => {
it('should handle multiple operations in sequence', async () => {
const { result } = renderHook(() => useTodoOperations(), { wrapper });
const operations = [
() => result.current.createTodo({
title: 'Tarea 1',
description: '',
completed: false,
userId: 'user-123',
categoryId: 'work',
priority: 'medium' as const,
dueDate: null,
tags: [],
}),
() => result.current.createTodo({
title: 'Tarea 2',
description: '',
completed: false,
userId: 'user-123',
categoryId: 'personal',
priority: 'high' as const,
dueDate: null,
tags: [],
}),
() => result.current.updateTodo('1', { completed: true }),
];
await act(async () => {
await Promise.all(operations.map(op => op()));
});
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});
});
});
🎭 Mocking Avanzado y Fixtures
Factory para Datos de Testing
// src/test/factories/todoFactory.ts
import { Todo, Category } from '@/types/store';
import { faker } from '@faker-js/faker';
export class TodoFactory {
static create(overrides: Partial<Todo> = {}): Todo {
return {
id: faker.string.uuid(),
title: faker.lorem.sentence({ min: 3, max: 8 }),
description: faker.lorem.paragraph(),
completed: faker.datatype.boolean(),
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
userId: faker.string.uuid(),
categoryId: faker.string.uuid(),
priority: faker.helpers.arrayElement(['low', 'medium', 'high']),
dueDate: faker.datatype.boolean() ? faker.date.future() : null,
tags: faker.lorem.words({ min: 1, max: 5 }).split(' '),
...overrides,
};
}
static createMany(count: number, overrides: Partial<Todo> = {}): Todo[] {
return Array.from({ length: count }, () => this.create(overrides));
}
static createCompleted(overrides: Partial<Todo> = {}): Todo {
return this.create({ completed: true, ...overrides });
}
static createPending(overrides: Partial<Todo> = {}): Todo {
return this.create({ completed: false, ...overrides });
}
static createOverdue(overrides: Partial<Todo> = {}): Todo {
return this.create({
dueDate: faker.date.past(),
completed: false,
...overrides,
});
}
static createByCategory(categoryId: string, count: number = 1): Todo[] {
return this.createMany(count, { categoryId });
}
}
export class CategoryFactory {
static create(overrides: Partial<Category> = {}): Category {
return {
id: faker.string.uuid(),
name: faker.commerce.department(),
color: faker.internet.color(),
icon: faker.helpers.arrayElement(['briefcase', 'home', 'heart', 'star']),
userId: faker.string.uuid(),
...overrides,
};
}
static createMany(count: number, overrides: Partial<Category> = {}): Category[] {
return Array.from({ length: count }, () => this.create(overrides));
}
}
Fixtures Complejas
// src/test/fixtures/userScenarios.ts
import { TodoFactory, CategoryFactory } from '../factories';
export const userScenarios = {
// Usuario productivo con muchas tareas
productiveUser: {
userId: 'productive-user',
categories: CategoryFactory.createMany(5, { userId: 'productive-user' }),
todos: [
...TodoFactory.createMany(15, {
userId: 'productive-user',
completed: false,
}),
...TodoFactory.createMany(25, {
userId: 'productive-user',
completed: true,
}),
],
},
// Usuario nuevo sin datos
newUser: {
userId: 'new-user',
categories: [],
todos: [],
},
// Usuario con tareas vencidas
procrastinatorUser: {
userId: 'procrastinator-user',
categories: CategoryFactory.createMany(3, { userId: 'procrastinator-user' }),
todos: [
...TodoFactory.createMany(8, {
userId: 'procrastinator-user',
dueDate: new Date(Date.now() - 86400000), // Ayer
completed: false,
}),
...TodoFactory.createMany(3, {
userId: 'procrastinator-user',
dueDate: new Date(Date.now() - 86400000 * 7), // Hace una semana
completed: false,
}),
],
},
// Usuario organizado
organizedUser: {
userId: 'organized-user',
categories: [
CategoryFactory.create({
id: 'work',
name: 'Trabajo',
userId: 'organized-user',
}),
CategoryFactory.create({
id: 'personal',
name: 'Personal',
userId: 'organized-user',
}),
],
todos: [
...TodoFactory.createByCategory('work', 10).map(todo => ({
...todo,
userId: 'organized-user',
priority: 'high' as const,
})),
...TodoFactory.createByCategory('personal', 5).map(todo => ({
...todo,
userId: 'organized-user',
priority: 'medium' as const,
})),
],
},
};
🔍 Testing de Integración
Testing de Flujos Completos
// src/__tests__/integration/todoFlow.test.tsx
import React from 'react';
import { render, fireEvent, waitFor, within } from '@testing-library/react-native';
import { App } from '@/App';
import { setupMSWGlobal } from '@/test/mocks/setup';
import { testScenarios } from '@/test/mocks/scenarios';
import { userScenarios } from '@/test/fixtures/userScenarios';
setupMSWGlobal();
describe('Todo Management Flow', () => {
beforeEach(() => {
testScenarios.normal();
});
it('should complete full todo lifecycle', async () => {
// Mock usuario autenticado
jest.mock('@/stores/authStore', () => ({
useAuthStore: () => ({
isAuthenticated: true,
user: { id: 'test-user' },
loading: false,
}),
}));
const { getByTestId, getByText } = render(<App />);
// 1. Verificar que carga la lista de todos
await waitFor(() => {
expect(getByTestId('todo-list')).toBeTruthy();
});
// 2. Crear nueva tarea
fireEvent.press(getByTestId('add-todo-button'));
await waitFor(() => {
expect(getByTestId('todo-form')).toBeTruthy();
});
fireEvent.changeText(getByTestId('title-input'), 'Nueva tarea de integración');
fireEvent.changeText(getByTestId('description-input'), 'Descripción de la tarea');
fireEvent.press(getByTestId('save-todo-button'));
// 3. Verificar que aparece en la lista
await waitFor(() => {
expect(getByText('Nueva tarea de integración')).toBeTruthy();
});
// 4. Marcar como completada
const todoItem = getByTestId('todo-item-nueva-tarea-de-integracion');
const checkbox = within(todoItem).getByTestId('todo-checkbox');
fireEvent.press(checkbox);
// 5. Verificar estado completado
await waitFor(() => {
expect(within(todoItem).getByTestId('completed-todo')).toBeTruthy();
});
// 6. Editar tarea
fireEvent.press(todoItem);
await waitFor(() => {
expect(getByTestId('todo-detail')).toBeTruthy();
});
fireEvent.press(getByTestId('edit-todo-button'));
await waitFor(() => {
expect(getByTestId('todo-form')).toBeTruthy();
});
fireEvent.changeText(getByTestId('title-input'), 'Tarea editada');
fireEvent.press(getByTestId('save-todo-button'));
// 7. Verificar cambios
await waitFor(() => {
expect(getByText('Tarea editada')).toBeTruthy();
});
// 8. Eliminar tarea
fireEvent.press(getByTestId('delete-todo-button'));
fireEvent.press(getByTestId('confirm-delete-button'));
// 9. Verificar que se eliminó
await waitFor(() => {
expect(() => getByText('Tarea editada')).toThrow();
});
});
it('should handle offline scenario', async () => {
// Simular estado offline
jest.mock('@react-native-community/netinfo', () => ({
addEventListener: jest.fn(),
getCurrentState: jest.fn(() => Promise.resolve({ isConnected: false })),
}));
testScenarios.serverError();
const { getByTestId, getByText } = render(<App />);
// Verificar mensaje de offline
await waitFor(() => {
expect(getByText('Sin conexión a internet')).toBeTruthy();
});
// Intentar crear tarea offline
fireEvent.press(getByTestId('add-todo-button'));
fireEvent.changeText(getByTestId('title-input'), 'Tarea offline');
fireEvent.press(getByTestId('save-todo-button'));
// Verificar que se guarda localmente
await waitFor(() => {
expect(getByText('Tarea offline')).toBeTruthy();
expect(getByTestId('sync-pending-indicator')).toBeTruthy();
});
});
});
✅ Scripts de Testing Avanzados
Configuración de Jest Avanzada
// package.json (actualización)
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:integration": "jest --testPathPattern=integration",
"test:unit": "jest --testPathPattern=__tests__ --testPathIgnorePatterns=integration",
"test:components": "jest --testPathPattern=components",
"test:hooks": "jest --testPathPattern=hooks",
"test:stores": "jest --testPathPattern=stores",
"test:e2e": "detox test",
"test:accessibility": "jest --testNamePattern='accessibility'",
"test:performance": "jest --testNamePattern='performance'",
"test:debug": "jest --runInBand --verbose",
"test:ci": "jest --ci --coverage --watchAll=false"
},
"jest": {
"preset": "react-native",
"setupFilesAfterEnv": [
"@testing-library/jest-native/extend-expect",
"<rootDir>/src/test/setup.ts"
],
"testMatch": [
"**/__tests__/**/*.(ts|tsx|js)",
"**/*.(test|spec).(ts|tsx|js)"
],
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"!src/**/*.d.ts",
"!src/test/**/*",
"!src/**/*.stories.*",
"!src/**/*.config.*"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
},
"coverageReporters": ["text", "lcov", "html"],
"transformIgnorePatterns": [
"node_modules/(?!(react-native|@react-native|react-native-reanimated|zustand|@react-navigation)/)"
],
"testEnvironment": "jsdom"
}
}
📝 Resumen
En este capítulo hemos:
- ✅ Configurado MSW avanzado con escenarios complejos
- ✅ Implementado testing de componentes con estados complejos
- ✅ Creado factories y fixtures profesionales
- ✅ Desarrollado testing de integración end-to-end
- ✅ Establecido métricas de coverage y calidad
Próximos Pasos
En el siguiente capítulo implementaremos:
- Configuración de Clerk para autenticación
- Testing de autenticación con MSW
- OAuth providers (Google, Apple)
- Protección de rutas
¡El testing está completamente configurado con las mejores prácticas! 🎉