Testing en Angular: Jest y Cypress para Calidad Total

16 de diciembre de 2025
Osman Jimenez
Angular Testing Calidad de Software

Testing Completo en Angular

Una estrategia de testing sólida es esencial para aplicaciones mantenibles. Aprende a implementar tests unitarios con Jest y E2E con Cypress.

Configurar Jest en Angular

// Instalar dependencias
npm install --save-dev jest @types/jest jest-preset-angular

// jest.config.js
module.exports = {
  preset: 'jest-preset-angular',
  setupFilesAfterEnv: ['/setup-jest.ts'],
  testPathIgnorePatterns: ['/node_modules/', '/dist/'],
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.spec.ts',
    '!src/main.ts'
  ]
};

// setup-jest.ts
import 'jest-preset-angular/setup-jest';

Tests Unitarios de Componentes

describe('UserListComponent', () => {
  let component: UserListComponent;
  let fixture: ComponentFixture;
  let userService: jest.Mocked;
  
  beforeEach(() => {
    const userServiceMock = {
      getUsers: jest.fn()
    };
    
    TestBed.configureTestingModule({
      imports: [UserListComponent],
      providers: [
        { provide: UserService, useValue: userServiceMock }
      ]
    });
    
    fixture = TestBed.createComponent(UserListComponent);
    component = fixture.componentInstance;
    userService = TestBed.inject(UserService) as jest.Mocked;
  });
  
  it('should load users on init', () => {
    const mockUsers = [{ id: 1, name: 'John' }];
    userService.getUsers.mockReturnValue(of(mockUsers));
    
    fixture.detectChanges();
    
    expect(component.users()).toEqual(mockUsers);
    expect(userService.getUsers).toHaveBeenCalledTimes(1);
  });
  
  it('should display users in template', () => {
    component.users.set([{ id: 1, name: 'John' }]);
    fixture.detectChanges();
    
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('.user-name').textContent).toContain('John');
  });
});

Tests de Servicios

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });
  
  afterEach(() => {
    httpMock.verify();
  });
  
  it('should fetch users', (done) => {
    const mockUsers = [{ id: 1, name: 'John' }];
    
    service.getUsers().subscribe(users => {
      expect(users).toEqual(mockUsers);
      done();
    });
    
    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
  
  it('should handle errors', (done) => {
    service.getUsers().subscribe({
      error: (error) => {
        expect(error.status).toBe(500);
        done();
      }
    });
    
    const req = httpMock.expectOne('/api/users');
    req.flush('Error', { status: 500, statusText: 'Server Error' });
  });
});

Tests con Signals

describe('CounterComponent with Signals', () => {
  it('should increment counter', () => {
    const fixture = TestBed.createComponent(CounterComponent);
    const component = fixture.componentInstance;
    
    expect(component.count()).toBe(0);
    
    component.increment();
    expect(component.count()).toBe(1);
    
    fixture.detectChanges();
    const button = fixture.nativeElement.querySelector('button');
    expect(button.textContent).toContain('1');
  });
});

Configurar Cypress

// Instalar
npm install --save-dev cypress

// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:4200',
    supportFile: 'cypress/support/e2e.ts',
    specPattern: 'cypress/e2e/**/*.cy.ts'
  },
  component: {
    devServer: {
      framework: 'angular',
      bundler: 'webpack'
    },
    specPattern: '**/*.cy.ts'
  }
});

Tests E2E con Cypress

// cypress/e2e/user-flow.cy.ts
describe('User Flow', () => {
  beforeEach(() => {
    cy.visit('/');
  });
  
  it('should login and view dashboard', () => {
    // Login
    cy.get('[data-cy=email]').type('user@example.com');
    cy.get('[data-cy=password]').type('password123');
    cy.get('[data-cy=login-btn]').click();
    
    // Verificar redirección
    cy.url().should('include', '/dashboard');
    
    // Verificar contenido
    cy.get('[data-cy=welcome-message]')
      .should('contain', 'Welcome');
  });
  
  it('should create new user', () => {
    cy.get('[data-cy=new-user-btn]').click();
    
    cy.get('[data-cy=name-input]').type('John Doe');
    cy.get('[data-cy=email-input]').type('john@example.com');
    cy.get('[data-cy=submit-btn]').click();
    
    // Verificar mensaje de éxito
    cy.get('[data-cy=success-message]')
      .should('be.visible')
      .and('contain', 'User created');
    
    // Verificar que aparece en la lista
    cy.get('[data-cy=user-list]')
      .should('contain', 'John Doe');
  });
});

Custom Commands en Cypress

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable;
      createUser(name: string, email: string): Chainable;
    }
  }
}

Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('[data-cy=email]').type(email);
  cy.get('[data-cy=password]').type(password);
  cy.get('[data-cy=login-btn]').click();
  cy.url().should('include', '/dashboard');
});

Cypress.Commands.add('createUser', (name, email) => {
  cy.request('POST', '/api/users', { name, email });
});

// Uso
cy.login('user@example.com', 'password');
cy.createUser('John', 'john@example.com');

Interceptar Requests

describe('API Mocking', () => {
  it('should mock API response', () => {
    cy.intercept('GET', '/api/users', {
      statusCode: 200,
      body: [
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' }
      ]
    }).as('getUsers');
    
    cy.visit('/users');
    cy.wait('@getUsers');
    
    cy.get('[data-cy=user-list]')
      .children()
      .should('have.length', 2);
  });
  
  it('should handle API errors', () => {
    cy.intercept('GET', '/api/users', {
      statusCode: 500,
      body: { error: 'Server Error' }
    });
    
    cy.visit('/users');
    
    cy.get('[data-cy=error-message]')
      .should('be.visible');
  });
});

Component Testing con Cypress

// user-card.component.cy.ts
import { UserCardComponent } from './user-card.component';

describe('UserCardComponent', () => {
  it('should display user info', () => {
    cy.mount(UserCardComponent, {
      componentProperties: {
        user: { id: 1, name: 'John', email: 'john@example.com' }
      }
    });
    
    cy.get('[data-cy=user-name]').should('contain', 'John');
    cy.get('[data-cy=user-email]').should('contain', 'john@example.com');
  });
  
  it('should emit delete event', () => {
    const onDeleteSpy = cy.spy().as('onDeleteSpy');
    
    cy.mount(UserCardComponent, {
      componentProperties: {
        user: { id: 1, name: 'John' },
        delete: onDeleteSpy
      }
    });
    
    cy.get('[data-cy=delete-btn]').click();
    cy.get('@onDeleteSpy').should('have.been.calledOnce');
  });
});

Mejores Prácticas

  1. Usa data-cy attributes: Para selectores estables
  2. Tests independientes: Cada test debe poder ejecutarse solo
  3. Arrange-Act-Assert: Estructura clara de tests
  4. Mock dependencies: Aisla lo que estás probando
  5. Coverage mínimo: Apunta a 80%+ en código crítico
  6. Tests rápidos: Unitarios < 1s, E2E < 30s

Conclusión

Una estrategia de testing completa con Jest para unitarios y Cypress para E2E te da confianza para refactorizar y agregar features. Invierte tiempo en tests y ahorrarás mucho más en debugging.