Frontend Testing Strategies: Unit, Integration, and E2E

The Testing Pyramid

A balanced testing strategy combines unit tests (fast, many), integration tests (medium), and E2E tests (slow, few). Each type catches different bugs.

Unit Testing with Jest

// Pure function test
describe('calculateTotal', () => {
  it('calculates total with tax', () => {
    const items = [
      { price: 100, quantity: 2 },
      { price: 50, quantity: 1 }
    ];
    
    expect(calculateTotal(items, 0.1)).toBe(275); // (200 + 50) * 1.1
  });
  
  it('returns 0 for empty cart', () => {
    expect(calculateTotal([], 0.1)).toBe(0);
  });
});

// Mocking dependencies
jest.mock('./api');
import { fetchUser } from './api';

describe('UserService', () => {
  it('fetches and transforms user data', async () => {
    (fetchUser as jest.Mock).mockResolvedValue({
      id: '1',
      first_name: 'John',
      last_name: 'Doe'
    });
    
    const user = await userService.getUser('1');
    
    expect(user).toEqual({
      id: '1',
      fullName: 'John Doe'
    });
  });
});

Component Testing with Testing Library

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('LoginForm', () => {
  it('shows validation errors for empty fields', async () => {
    render();
    
    await userEvent.click(screen.getByRole('button', { name: /login/i }));
    
    expect(screen.getByText(/email is required/i)).toBeInTheDocument();
    expect(screen.getByText(/password is required/i)).toBeInTheDocument();
  });
  
  it('calls onSubmit with form data', async () => {
    const handleSubmit = jest.fn();
    render();
    
    await userEvent.type(
      screen.getByLabelText(/email/i),
      'test@example.com'
    );
    await userEvent.type(
      screen.getByLabelText(/password/i),
      'password123'
    );
    await userEvent.click(screen.getByRole('button', { name: /login/i }));
    
    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123'
    });
  });
  
  it('disables button while loading', async () => {
    render();
    
    expect(screen.getByRole('button', { name: /loading/i })).toBeDisabled();
  });
});

Angular Testing

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';

describe('UserListComponent', () => {
  let component: UserListComponent;
  let fixture: ComponentFixture;
  let userService: jasmine.SpyObj;
  
  beforeEach(async () => {
    const spy = jasmine.createSpyObj('UserService', ['getUsers']);
    
    await TestBed.configureTestingModule({
      imports: [UserListComponent, HttpClientTestingModule],
      providers: [{ provide: UserService, useValue: spy }]
    }).compileComponents();
    
    userService = TestBed.inject(UserService) as jasmine.SpyObj;
    fixture = TestBed.createComponent(UserListComponent);
    component = fixture.componentInstance;
  });
  
  it('displays users from service', async () => {
    userService.getUsers.and.returnValue(of([
      { id: '1', name: 'John' },
      { id: '2', name: 'Jane' }
    ]));
    
    fixture.detectChanges();
    await fixture.whenStable();
    
    const items = fixture.nativeElement.querySelectorAll('.user-item');
    expect(items.length).toBe(2);
  });
});

E2E Testing with Playwright

import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('user can login successfully', async ({ page }) => {
    await page.goto('/login');
    
    await page.fill('[data-testid="email"]', 'user@example.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="submit"]');
    
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('h1')).toHaveText('Welcome back!');
  });
  
  test('shows error for invalid credentials', async ({ page }) => {
    await page.goto('/login');
    
    await page.fill('[data-testid="email"]', 'wrong@example.com');
    await page.fill('[data-testid="password"]', 'wrongpassword');
    await page.click('[data-testid="submit"]');
    
    await expect(page.locator('.error-message')).toHaveText(
      'Invalid email or password'
    );
  });
});

test.describe('Shopping Cart', () => {
  test.beforeEach(async ({ page }) => {
    // Login before each test
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@example.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="submit"]');
  });
  
  test('user can add items to cart', async ({ page }) => {
    await page.goto('/products');
    await page.click('[data-testid="add-to-cart-1"]');
    
    const cartCount = page.locator('[data-testid="cart-count"]');
    await expect(cartCount).toHaveText('1');
  });
});

API Mocking with MSW

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.json([
      { id: '1', name: 'John' },
      { id: '2', name: 'Jane' }
    ]));
  }),
  
  rest.post('/api/login', async (req, res, ctx) => {
    const { email, password } = await req.json();
    
    if (email === 'valid@example.com' && password === 'password') {
      return res(ctx.json({ token: 'fake-token' }));
    }
    
    return res(ctx.status(401), ctx.json({ error: 'Invalid credentials' }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Testing Strategy

TypeRatioFocus
Unit70%Logic, utilities, services
Integration20%Components, hooks, state
E2E10%Critical user journeys

Conclusion

Good tests give confidence to refactor and ship. Focus on testing behavior, not implementation. Use the right tool for each level of the testing pyramid.

๐Ÿ“šTechnical Insights & Industry Trends

Insights & Knowledge Hub

Explore in-depth articles on software development, digital transformation, and emerging technologies. Learn from real-world experience.

Browse by Topic

๐Ÿ…ฐ๏ธ
ArchitectureJan 2024

Why Angular SSR Beats React for SEO in 2024

A comprehensive comparison of server-side rendering implementations and their impact on search engine optimization, performance, and user experience.

๐Ÿ”Œ
ArchitectureDec 2023

Microservices: When to Use and When to Avoid

Understanding the trade-offs of microservices architecture and making the right choice for your organization's scale, complexity, and team structure.

๐Ÿ—๏ธ
System ArchitectureJan 2024

Essential Design Patterns for Enterprise Systems

Master the fundamental design patterns that every software architect should know for building scalable, maintainable enterprise applications.

๐Ÿ—๏ธ
System ArchitectureJan 2024

Domain-Driven Design: A Practical Guide

Learn how to apply Domain-Driven Design principles to create software that truly reflects business requirements and scales with organizational complexity.

๐Ÿ—๏ธ
System ArchitectureDec 2023

Event-Driven Architecture: Complete Implementation Guide

Build loosely coupled, scalable systems using event-driven architecture patterns with practical examples using Kafka, RabbitMQ, and Azure Service Bus.

๐Ÿ—๏ธ
System ArchitectureDec 2023

RESTful API Design: Best Practices and Patterns

Design APIs that developers love to use. Learn REST principles, versioning strategies, error handling, and documentation best practices.

Deep Dives into Key Areas

๐Ÿ—๏ธ

System Architecture

Design patterns and architectural decisions

12 Articles
โ˜๏ธ

Cloud Computing

AWS, Azure, and cloud-native development

8 Articles
โšก

Performance

Optimization and scalability techniques

6 Articles
๐Ÿค–

AI & Machine Learning

Practical AI applications in enterprise

5 Articles
๐Ÿ”’

Security

Best practices and compliance

7 Articles
๐Ÿ“ฑ

Modern Frontend

Angular, React, and UI/UX best practices

10 Articles

Have a Project in Mind?

Let's turn your ideas into reality. Schedule a free consultation to discuss how we can help transform your business.