Convenciones de Testing — Finnova
Este documento define la estrategia y convenciones de testing para el proyecto Finnova. El objetivo inicial es tener cobertura básica en lógica crítica. No se exige cobertura total en MVP.
🧩 Tipos de tests
Unit tests (tests/unit/)
Prueban una función o clase de forma aislada, sin base de datos ni red. Si la función depende de algo externo, se mockea.
Cuándo escribirlos: lógica de negocio en servicios, funciones utilitarias, cálculos, transformaciones de datos.
// auth.service.test.ts
// Testeamos que el servicio hashea la contraseña antes de guardar,
// sin tocar la base de datos real — el repositorio se mockea.
describe('authService.register', () => {
it('debería hashear la contraseña antes de guardarla', async () => {
const mockRepo = { save: jest.fn().mockResolvedValue({ id: '1' }) };
const service = new AuthService(mockRepo);
await service.register({ email: 'test@finnova.com', password: '1234' });
const savedUser = mockRepo.save.mock.calls[0][0];
expect(savedUser.password).not.toBe('1234'); // no guardó en texto plano
expect(savedUser.password).toMatch(/^\$2[ab]\$/); // tiene formato bcrypt
});
});
Tests de integración (tests/integration/)
Prueban que varios componentes funcionan juntos: el endpoint HTTP recibe la request, pasa por el controlador, el servicio y llega a la base de datos real (de test).
Cuándo escribirlos: endpoints de la API, flujos completos de un módulo (registro, login, creación de transacción).
// auth.routes.test.ts
// Levantamos la app completa contra una base de datos de test.
// Verificamos que el endpoint responde correctamente de punta a punta.
describe('POST /auth/register', () => {
it('debería crear un usuario y retornar 201 con token', async () => {
const res = await request(app)
.post('/auth/register')
.send({ email: 'nuevo@finnova.com', password: 'segura123' });
expect(res.status).toBe(201);
expect(res.body).toHaveProperty('token');
expect(res.body.user.email).toBe('nuevo@finnova.com');
});
it('debería retornar 409 si el email ya existe', async () => {
await createUser({ email: 'existe@finnova.com' }); // seed en la db de test
const res = await request(app)
.post('/auth/register')
.send({ email: 'existe@finnova.com', password: 'segura123' });
expect(res.status).toBe(409);
});
});
Tests de componentes UI (tests/components/)
Prueban que un componente de React Native renderiza y responde a interacciones correctamente, sin levantar la app completa.
Cuándo escribirlos: componentes con lógica condicional visible (mostrar/ocultar, estados de carga, mensajes de error).
// LoginForm.test.tsx
// Verificamos que el botón se deshabilita mientras carga
// y que se muestra el error cuando las credenciales son incorrectas.
describe('LoginForm', () => {
it('debería deshabilitar el botón mientras carga', () => {
const { getByText } = render(<LoginForm isLoading={true} onSubmit={jest.fn()} />);
expect(getByText('Ingresar')).toBeDisabled();
});
it('debería mostrar mensaje de error si las credenciales son incorrectas', () => {
const { getByText } = render(
<LoginForm isLoading={false} error="Credenciales inválidas" onSubmit={jest.fn()} />
);
expect(getByText('Credenciales inválidas')).toBeTruthy();
});
});
🎯 Qué testear
| Prioridad | Tipo | Qué | Dónde |
|---|---|---|---|
| Alta | Unit | Servicios del backend (lógica de negocio) | tests/unit/ |
| Alta | Integración | Endpoints de API | tests/integration/ |
| Media | Unit | Funciones utilitarias en shared/lib/ | tests/unit/ |
| Baja | Componente | Componentes UI con lógica condicional | tests/components/ |
📁 Estructura de archivos
- Archivos de test con sufijo
.test.tso.spec.ts. - El archivo de test vive junto al archivo que testea, o en la carpeta
tests/replicando la misma estructura desrc/.
src/
modules/
auth/
auth.service.ts
auth.service.test.ts ← junto al archivo
tests/
integration/
auth.routes.test.ts ← o en tests/ replicando estructura
✍️ Cómo escribir tests
Nombres
- Nombres de tests en español, describiendo el comportamiento esperado, no la implementación.
- Usar
describepara agrupar por módulo o función.
// ✅
describe('userService', () => {
it('debería lanzar un error si el email ya existe', async () => { ... });
it('debería retornar el usuario creado con id generado', async () => { ... });
});
// ❌
it('test 1', () => { ... });
it('works', () => { ... });
describe('test suite', () => { ... });
Qué y cómo testear
- Testear comportamiento observable, no implementación interna.
- Preferir datos de prueba explícitos sobre mocks masivos — lo que se ve en el test debe ser suficiente para entenderlo sin leer el código fuente.
- No mockear la base de datos en tests de integración — usar una base de datos de test real para evitar divergencias.
// ✅ Datos explícitos, comportamiento claro
it('debería calcular el saldo total correctamente', () => {
const transactions = [
{ type: 'income', amount: 1000 },
{ type: 'expense', amount: 300 },
];
expect(calculateBalance(transactions)).toBe(700);
});
// ❌ Mock excesivo que oculta el comportamiento
it('calcula saldo', () => {
jest.mock('../transactions');
expect(getBalance()).toBeDefined();
});
🛠️ Herramientas
| Herramienta | Uso |
|---|---|
| Jest | Framework principal para unit e integration tests (backend) |
| Supertest | Testing de endpoints HTTP en el backend |
| React Native Testing Library | Testing de componentes UI en el frontend |