workspace/projects/gamilit/docs/95-guias-desarrollo/TESTING-GUIDE.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- Configure workspace Git repository with comprehensive .gitignore
- Add Odoo as submodule for ERP reference code
- Include documentation: SETUP.md, GIT-STRUCTURE.md
- Add gitignore templates for projects (backend, frontend, database)
- Structure supports independent repos per project/subproject level

Workspace includes:
- core/ - Reusable patterns, modules, orchestration system
- projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.)
- knowledge-base/ - Reference code and patterns (includes Odoo submodule)
- devtools/ - Development tools and templates
- customers/ - Client implementations template

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 10:44:23 -06:00

20 KiB

Guía de Testing GAMILIT

Proyecto: GAMILIT - Consolidación GAMILIT Platform Fecha: 2025-10-27 Versión: 1.0


Introducción

Esta guía presenta ejemplos prácticos de testing para el proyecto GAMILIT, cubriendo tanto el backend (Node.js/Express con Jest) como el frontend (React/Vite con Vitest). El objetivo de cobertura es 80% como mínimo en todas las capas críticas: servicios, repositorios, controladores (backend) y componentes, stores, hooks (frontend).

GAMILIT utiliza un stack de testing moderno que garantiza la calidad del código mediante tests unitarios, de integración y end-to-end. El backend emplea Jest con soporte para TypeScript, mientras que el frontend utiliza Vitest + React Testing Library para una experiencia de testing rápida y confiable.


Stack de Testing

Backend

  • Framework: Jest 29.x
  • Mocking: jest.mock(), jest.fn()
  • Coverage: Istanbul integrado en Jest
  • Supertest: Para tests de integración HTTP
  • TypeScript: ts-jest

Frontend

  • Framework: Vitest 1.x (compatible con Jest API)
  • Testing Library: @testing-library/react 14.x
  • User Events: @testing-library/user-event
  • Mocking: vi.mock() (Vitest)
  • Coverage: c8 o vitest coverage

Configuración Básica

jest.config.js (Backend)

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.test.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.types.ts',
    '!src/**/*.d.ts',
    '!src/server.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^@shared/(.*)$': '<rootDir>/src/shared/$1',
    '^@modules/(.*)$': '<rootDir>/src/modules/$1',
  },
};

vitest.config.ts (Frontend)

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts',
    coverage: {
      provider: 'c8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*',
        '**/mockData',
        'src/main.tsx',
      ],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80,
      },
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@shared': path.resolve(__dirname, './src/shared'),
      '@features': path.resolve(__dirname, './src/features'),
      '@apps': path.resolve(__dirname, './src/apps'),
    },
  },
});

Tests de Backend

1. AuthService.login() - Test Unitario

Descripción: Test del servicio de autenticación que valida credenciales y retorna tokens JWT.

import { AuthService } from '../auth.service';
import { UsersRepository } from '../users.repository';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';

jest.mock('../users.repository');
jest.mock('bcryptjs');
jest.mock('jsonwebtoken');

describe('AuthService', () => {
  let authService: AuthService;
  let mockUsersRepo: jest.Mocked<UsersRepository>;

  beforeEach(() => {
    mockUsersRepo = new UsersRepository() as jest.Mocked<UsersRepository>;
    authService = new AuthService(mockUsersRepo);
  });

  describe('login()', () => {
    it('should return access and refresh tokens on valid credentials', async () => {
      const mockUser = {
        id: 'user-123',
        email: 'student@glit.com',
        password_hash: 'hashed_password',
        role: 'student',
        status: 'active',
      };

      mockUsersRepo.findByEmail.mockResolvedValue(mockUser);
      (bcrypt.compare as jest.Mock).mockResolvedValue(true);
      (jwt.sign as jest.Mock).mockReturnValue('mock_token');

      const result = await authService.login('student@glit.com', 'Test1234');

      expect(result).toHaveProperty('accessToken');
      expect(result).toHaveProperty('refreshToken');
      expect(result.user.email).toBe('student@glit.com');
      expect(mockUsersRepo.findByEmail).toHaveBeenCalledWith('student@glit.com');
    });
  });
});

Explicación: Mock del repositorio y dependencias externas (bcrypt, jwt). Valida que el servicio retorna los tokens esperados cuando las credenciales son correctas.


2. ExerciseRepository.findById() - Test con Mock de DB

Descripción: Test del repositorio que busca ejercicios por ID usando un pool de PostgreSQL mockeado.

import { ExerciseRepository } from '../exercise.repository';
import { pool } from '@/database/pool';

jest.mock('@/database/pool');

describe('ExerciseRepository', () => {
  let repo: ExerciseRepository;
  let mockPool: jest.Mocked<typeof pool>;

  beforeEach(() => {
    mockPool = pool as jest.Mocked<typeof pool>;
    repo = new ExerciseRepository();
  });

  describe('findById()', () => {
    it('should return exercise when found', async () => {
      const mockExercise = {
        id: 'ex-123',
        module_id: 'mod-1',
        title: 'Crucigrama Maya',
        type: 'crucigrama',
        config: { size: 10, words: [] },
        xp_reward: 100,
        ml_coins_reward: 50,
      };

      mockPool.query.mockResolvedValue({
        rows: [mockExercise],
        rowCount: 1,
      } as any);

      const result = await repo.findById('ex-123');

      expect(result).toEqual(mockExercise);
      expect(mockPool.query).toHaveBeenCalledWith(
        'SELECT * FROM exercises WHERE id = $1',
        ['ex-123']
      );
    });
  });
});

Explicación: Mock del pool de PostgreSQL. Valida que la query SQL es correcta y que el repositorio retorna el objeto esperado del resultado de la base de datos.


3. POST /api/auth/login - Test de Integración

Descripción: Test de integración que valida el endpoint completo de login incluyendo validación, middlewares y respuesta HTTP.

import request from 'supertest';
import { createApp } from '@/app';
import { pool } from '@/database/pool';

describe('POST /api/auth/login', () => {
  let app: Express.Application;

  beforeAll(async () => {
    app = createApp();
  });

  afterAll(async () => {
    await pool.end();
  });

  it('should return 200 and tokens on valid credentials', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({
        email: 'student@glit.com',
        password: 'Test1234',
      })
      .expect(200);

    expect(response.body.success).toBe(true);
    expect(response.body.data).toHaveProperty('accessToken');
    expect(response.body.data).toHaveProperty('refreshToken');
    expect(response.body.data.user.email).toBe('student@glit.com');
  });

  it('should return 400 on missing email', async () => {
    const response = await request(app)
      .post('/api/auth/login')
      .send({ password: 'Test1234' })
      .expect(400);

    expect(response.body.success).toBe(false);
    expect(response.body.error.code).toBe('VALIDATION_ERROR');
  });
});

Explicación: Usa Supertest para hacer requests HTTP reales a la aplicación Express. Valida el flujo completo desde el endpoint hasta la respuesta, incluyendo status codes y estructura de datos.


4. Middleware de Autenticación - Test de Middleware

Descripción: Test del middleware que valida JWT en requests protegidos.

import { Request, Response, NextFunction } from 'express';
import { authenticate } from '../auth.middleware';
import { AuthRequest } from '@/shared/types';
import jwt from 'jsonwebtoken';

jest.mock('jsonwebtoken');

describe('Auth Middleware', () => {
  let mockReq: Partial<AuthRequest>;
  let mockRes: Partial<Response>;
  let mockNext: NextFunction;

  beforeEach(() => {
    mockReq = {
      headers: {},
    };
    mockRes = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn().mockReturnThis(),
    };
    mockNext = jest.fn();
  });

  it('should attach user to request on valid token', () => {
    mockReq.headers = {
      authorization: 'Bearer valid_token',
    };

    const mockPayload = {
      id: 'user-123',
      email: 'student@glit.com',
      role: 'student',
    };

    (jwt.verify as jest.Mock).mockReturnValue(mockPayload);

    authenticate(mockReq as AuthRequest, mockRes as Response, mockNext);

    expect(mockReq.user).toEqual(mockPayload);
    expect(mockNext).toHaveBeenCalled();
  });

  it('should return 401 on missing token', () => {
    authenticate(mockReq as AuthRequest, mockRes as Response, mockNext);

    expect(mockRes.status).toHaveBeenCalledWith(401);
    expect(mockNext).not.toHaveBeenCalled();
  });
});

Explicación: Mock de Express request/response. Valida que el middleware decodifica correctamente el JWT y adjunta el usuario al request, o retorna 401 si falta el token.


5. Validación de DTO con Joi - Test de Validación

Descripción: Test de esquema de validación Joi para DTOs de entrada.

import Joi from 'joi';
import { loginSchema } from '../auth.validation';

describe('Auth Validation', () => {
  describe('loginSchema', () => {
    it('should validate correct login data', () => {
      const validData = {
        email: 'student@glit.com',
        password: 'Test1234',
      };

      const { error, value } = loginSchema.validate(validData);

      expect(error).toBeUndefined();
      expect(value).toEqual(validData);
    });

    it('should reject invalid email format', () => {
      const invalidData = {
        email: 'not-an-email',
        password: 'Test1234',
      };

      const { error } = loginSchema.validate(invalidData);

      expect(error).toBeDefined();
      expect(error?.details[0].message).toContain('valid email');
    });

    it('should reject weak passwords', () => {
      const weakPassword = {
        email: 'student@glit.com',
        password: '123',
      };

      const { error } = loginSchema.validate(weakPassword);

      expect(error).toBeDefined();
      expect(error?.details[0].path).toContain('password');
    });
  });
});

Explicación: Valida que el esquema Joi rechaza datos inválidos y acepta datos válidos. Útil para garantizar que las validaciones de entrada funcionan antes de llegar al controlador.


Tests de Frontend

6. LoginPage Component - Test de Renderizado

Descripción: Test del componente LoginPage que verifica que todos los elementos se renderizan correctamente.

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import LoginPage from '../LoginPage';

vi.mock('react-router-dom', async () => {
  const actual = await vi.importActual('react-router-dom');
  return {
    ...actual,
    useNavigate: () => vi.fn(),
  };
});

vi.mock('@features/auth/hooks/useAuth', () => ({
  useAuth: () => ({
    login: vi.fn(),
    isLoading: false,
    error: null,
  }),
}));

describe('LoginPage', () => {
  const renderComponent = () => {
    return render(
      <BrowserRouter>
        <LoginPage />
      </BrowserRouter>
    );
  };

  it('should render login form with all fields', () => {
    renderComponent();

    expect(screen.getByText('GAMILIT Detective Platform')).toBeInTheDocument();
    expect(screen.getByPlaceholderText(/detective@glit.com/i)).toBeInTheDocument();
    expect(screen.getByPlaceholderText(/••••••••/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /iniciar sesión/i })).toBeInTheDocument();
    expect(screen.getByRole('checkbox')).toBeInTheDocument();
  });

  it('should display form labels', () => {
    renderComponent();

    expect(screen.getByText('Email')).toBeInTheDocument();
    expect(screen.getByText('Contraseña')).toBeInTheDocument();
    expect(screen.getByText('Recordarme')).toBeInTheDocument();
  });
});

Explicación: Renderiza el componente con los providers necesarios (BrowserRouter) y verifica que todos los elementos del formulario estén presentes en el DOM usando queries de Testing Library.


7. authStore.login() - Test de Zustand Store

Descripción: Test del store de autenticación Zustand que gestiona el estado de login.

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { useAuthStore } from '../authStore';
import { authAPI } from '@features/auth/api/authAPI';

vi.mock('@features/auth/api/authAPI');

describe('authStore', () => {
  beforeEach(() => {
    useAuthStore.setState({
      user: null,
      token: null,
      isAuthenticated: false,
    });
  });

  describe('login()', () => {
    it('should set user and token on successful login', async () => {
      const mockResponse = {
        user: {
          id: 'user-123',
          email: 'student@glit.com',
          fullName: 'Test Student',
          role: 'student',
        },
        accessToken: 'mock_token',
      };

      vi.mocked(authAPI.login).mockResolvedValue(mockResponse);

      await useAuthStore.getState().login('student@glit.com', 'Test1234');

      const state = useAuthStore.getState();
      expect(state.user).toEqual(mockResponse.user);
      expect(state.token).toBe('mock_token');
      expect(state.isAuthenticated).toBe(true);
    });
  });
});

Explicación: Mock de la API y validación de que el store actualiza correctamente su estado interno (user, token, isAuthenticated) después de un login exitoso.


8. useExercise Hook - Test de Custom Hook

Descripción: Test del custom hook que gestiona el estado de un ejercicio.

import { describe, it, expect, vi } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useExercise } from '../useExercise';
import { exercisesAPI } from '@features/education/api/exercisesAPI';

vi.mock('@features/education/api/exercisesAPI');

describe('useExercise', () => {
  it('should fetch and return exercise data', async () => {
    const mockExercise = {
      id: 'ex-123',
      title: 'Crucigrama Maya',
      type: 'crucigrama',
      config: { size: 10 },
    };

    vi.mocked(exercisesAPI.getById).mockResolvedValue(mockExercise);

    const { result } = renderHook(() => useExercise('ex-123'));

    expect(result.current.isLoading).toBe(true);

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    expect(result.current.exercise).toEqual(mockExercise);
    expect(result.current.error).toBeNull();
  });

  it('should handle error when exercise not found', async () => {
    vi.mocked(exercisesAPI.getById).mockRejectedValue(new Error('Not found'));

    const { result } = renderHook(() => useExercise('invalid-id'));

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    expect(result.current.exercise).toBeNull();
    expect(result.current.error).toBeTruthy();
  });
});

Explicación: Usa renderHook de Testing Library para testear el ciclo de vida del hook. Valida estados de loading, data y error, y que se llama correctamente a la API.


9. Button Component - Test de Interacción

Descripción: Test del componente Button que valida eventos de click y variantes visuales.

import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../Button';

describe('Button', () => {
  it('should call onClick when clicked', async () => {
    const handleClick = vi.fn();
    const user = userEvent.setup();

    render(<Button onClick={handleClick}>Click me</Button>);

    const button = screen.getByRole('button', { name: /click me/i });
    await user.click(button);

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('should not call onClick when disabled', async () => {
    const handleClick = vi.fn();
    const user = userEvent.setup();

    render(<Button onClick={handleClick} disabled>Click me</Button>);

    const button = screen.getByRole('button', { name: /click me/i });
    await user.click(button);

    expect(handleClick).not.toHaveBeenCalled();
    expect(button).toBeDisabled();
  });

  it('should apply variant classes correctly', () => {
    render(<Button variant="primary">Primary</Button>);

    const button = screen.getByRole('button', { name: /primary/i });
    expect(button).toHaveClass('bg-blue-600');
  });
});

Explicación: Valida interacciones de usuario con userEvent. Verifica que el callback onClick se ejecuta, que el botón disabled no responde a clicks, y que las variantes aplican las clases CSS correctas.


10. API Client Utility - Test de Utility Function

Descripción: Test de la función utilitaria del cliente API que maneja requests HTTP con interceptors.

import { describe, it, expect, vi, beforeEach } from 'vitest';
import axios from 'axios';
import { apiClient } from '../apiClient';

vi.mock('axios');

describe('apiClient', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should add authorization header when token exists', async () => {
    const mockToken = 'mock_jwt_token';
    localStorage.setItem('auth-token', mockToken);

    vi.mocked(axios.create).mockReturnValue({
      get: vi.fn().mockResolvedValue({ data: { success: true } }),
      interceptors: {
        request: { use: vi.fn() },
        response: { use: vi.fn() },
      },
    } as any);

    const client = apiClient;
    await client.get('/api/test');

    expect(axios.create).toHaveBeenCalled();
  });

  it('should handle 401 responses and clear auth state', async () => {
    const mockError = {
      response: { status: 401 },
    };

    vi.mocked(axios.create).mockReturnValue({
      get: vi.fn().mockRejectedValue(mockError),
      interceptors: {
        request: { use: vi.fn() },
        response: { use: vi.fn((success, error) => error(mockError)) },
      },
    } as any);

    try {
      await apiClient.get('/api/protected');
    } catch (error: any) {
      expect(error.response.status).toBe(401);
    }
  });
});

Explicación: Mock de axios para testear el cliente API. Valida que los interceptors añaden headers de autenticación y manejan correctamente errores HTTP 401 (unauthorized).


Comandos de Testing

Backend (Jest)

# Ejecutar todos los tests
npm test

# Tests en modo watch
npm run test:watch

# Coverage report
npm run test:coverage

# Tests específicos
npm test -- auth.service.test.ts

# Tests en modo debug
node --inspect-brk node_modules/.bin/jest --runInBand

Frontend (Vitest)

# Ejecutar todos los tests
npm run test

# Tests en modo watch (UI interactiva)
npm run test:ui

# Coverage report
npm run test:coverage

# Tests específicos
npm run test -- LoginPage.test.tsx

# Tests con verbose output
npm run test -- --reporter=verbose

Mejores Prácticas

General

  1. AAA Pattern: Arrange (preparar), Act (actuar), Assert (verificar)
  2. Test Isolation: Cada test debe ser independiente
  3. Descriptive Names: Nombres descriptivos (should/when/given)
  4. Mock External Dependencies: Mockear APIs, DBs, servicios externos
  5. Coverage != Quality: 80% de cobertura es el objetivo, pero tests significativos son más importantes

Backend

  • Mockear pool de DB en tests unitarios
  • Usar Supertest para tests de integración HTTP
  • Testear edge cases y errores de validación
  • Verificar que middlewares se ejecutan en el orden correcto
  • Testear tanto paths exitosos como errores

Frontend

  • Usar queries semánticas (getByRole, getByLabelText)
  • Testear desde la perspectiva del usuario
  • Evitar testear detalles de implementación
  • Usar waitFor para operaciones asíncronas
  • Mock de hooks/stores solo cuando sea necesario

Referencias

Documentación Oficial

Documentación Interna GAMILIT

  • /docs/projects/gamilit/03-desarrollo/backend/ESTRUCTURA-Y-MODULOS.md
  • /docs/projects/gamilit/03-desarrollo/frontend/ESTRUCTURA-Y-FEATURES.md
  • /docs/projects/gamilit/03-desarrollo/backend/SERVICIOS-PRINCIPALES.md

Código de Ejemplo

  • Backend Tests: /projects/glit/backend/src/__tests__/
  • Frontend Tests: /gamilit-platform-web/src/**/__tests__/

Documento generado: 2025-10-27 Versión: 1.0 Autor: Equipo GAMILIT Cobertura objetivo: 80%