Shortcuts
BlogApril 20, 2025

Mohamed Elbarry
Testing Strategies
The testing pyramid suggests you should have more unit tests than integration tests, and more integration tests than end-to-end tests. This structure provides good coverage while maintaining fast feedback loops. Unit tests focus on testing individual functions or components in isolation. They should be fast, reliable, and test one specific behavior.
Unit Test Setup
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { UserService } from '../services/UserService';

// Mock external dependencies
vi.mock('../api/userApi', () => ({
fetchUser: vi.fn(),
updateUser: vi.fn()
}));

describe('UserService', () => {
let userService: UserService;

beforeEach(() => {
userService = new UserService();
});

describe('createUser', () => {
it('should create a user with valid data', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
};

    const result = await userService.createUser(userData);

    expect(result).toMatchObject({
      id: expect.any(String),
      name: userData.name,
      email: userData.email
    });
  });

});
});
Component Testing
import { render, screen, fireEvent } from '@testing-library/react';
import { UserCard } from '../components/UserCard';

describe('UserCard', () => {
const mockUser = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://example.com/avatar.png'
};

it('should render user information', () => {
render(<UserCard user={mockUser} />);

  expect(screen.getByText('John Doe')).toBeInTheDocument();
  expect(screen.getByText('john@example.com')).toBeInTheDocument();

});

it('should call onEdit when edit button is clicked', () => {
const mockOnEdit = vi.fn();
render(<UserCard user={mockUser} onEdit={mockOnEdit} />);

  fireEvent.click(screen.getByRole('button', { name: /edit/i }));

  expect(mockOnEdit).toHaveBeenCalledWith(mockUser);

});
});
Integration tests verify that different parts of your application work together correctly.
API Integration Tests
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import app from '../app';

describe('User API Integration', () => {
beforeEach(async () => {
await setupTestDatabase();
});

afterEach(async () => {
await cleanupTestDatabase();
});

describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
};

    const response = await request(app)
      .post('/api/users')
      .send(userData)
      .expect(201);

    expect(response.body).toMatchObject({
      id: expect.any(String),
      name: userData.name,
      email: userData.email
    });
    expect(response.body.password).toBeUndefined();
  });

});
});
E2E tests simulate real user interactions with your application.
E2E Tests
import { test, expect } from '@playwright/test';

test.describe('User Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'admin@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
});

test('should create a new user', async ({ page }) => {
await page.goto('/users');

  // Click create user button
  await page.click('[data-testid="create-user-button"]');

  // Fill user form
  await page.fill('[data-testid="user-name"]', 'John Doe');
  await page.fill('[data-testid="user-email"]', 'john@example.com');
  await page.fill('[data-testid="user-password"]', 'password123');

  // Submit form
  await page.click('[data-testid="submit-button"]');

  // Verify user was created
  await expect(page.locator('[data-testid="user-list"]')).toContainText('John Doe');

});
});
TDD is a development approach where you write tests before writing the implementation code.
TDD Example
// 1. Write a failing test
describe('UserService', () => {
describe('validateEmail', () => {
  it('should return true for valid email', () => {
    const userService = new UserService();
    expect(userService.validateEmail('test@example.com')).toBe(true);
  });

  it('should return false for invalid email', () => {
    const userService = new UserService();
    expect(userService.validateEmail('invalid-email')).toBe(false);
  });

});
});

// 2. Write minimal code to make test pass
export class UserService {
validateEmail(email: string): boolean {
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
return emailRegex.test(email);
}
}

// 3. Refactor if needed
// 4. Repeat the cycle
Mocking
import { vi } from 'vitest';

// Mock external API
vi.mock('../services/apiService', () => ({
fetchUser: vi.fn(),
updateUser: vi.fn()
}));

// Mock database
vi.mock('../database/connection', () => ({
query: vi.fn()
}));

describe('UserService with mocks', () => {
it('should fetch user from API', async () => {
const mockFetchUser = vi.mocked(fetchUser);
mockFetchUser.mockResolvedValue({
id: '1',
name: 'John Doe',
email: 'john@example.com'
});

  const userService = new UserService();
  const user = await userService.getUser('1');

  expect(mockFetchUser).toHaveBeenCalledWith('1');
  expect(user).toMatchObject({
    id: '1',
    name: 'John Doe',
    email: 'john@example.com'
  });

});
});
Load Testing
# Load testing with Artillery
config:
target: 'http://localhost:3000'
phases:
  - duration: 60
    arrivalRate: 10
  - duration: 120
    arrivalRate: 20

scenarios:

- name: "User API Load Test"
weight: 70
flow: - post:
url: "/api/users"
json:
name: "Test User {{ $randomString() }}"
email: "test{{ $randomString() }}@example.com"
password: "password123"
  • Write tests first - Use TDD when possible
  • Test behavior, not implementation - Focus on what the code does
  • Keep tests simple and focused - One test should verify one behavior
  • Use descriptive test names - Make it clear what each test verifies
  • Mock external dependencies - Isolate the code under test
  • Maintain good test coverage - Aim for 80%+ coverage
  • Run tests frequently - Integrate testing into your development workflow
  • Keep tests fast - Unit tests should run in milliseconds
  • Make tests reliable - Avoid flaky tests
  • Document complex test scenarios - Add comments for complex logic
The goal of testing is not just to find bugs, but to give you confidence in your code and make refactoring safer.
Share this post: