Portafolio profesional

Testing en Angular: Jest y Cypress para Calidad Total

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

Osman Jimenez

Testing en Angular: Jest y Cypress para Calidad Total

16 de diciembre de 2025 publicado 4 min lectura 3 temas

Temas

  • 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.