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
- Usa data-cy attributes: Para selectores estables
- Tests independientes: Cada test debe poder ejecutarse solo
- Arrange-Act-Assert: Estructura clara de tests
- Mock dependencies: Aisla lo que estás probando
- Coverage mínimo: Apunta a 80%+ en código crítico
- 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.