Skip to main content
Project Status: ProyectoWeb is currently in the planning stage. This guide provides general testing best practices that can be applied when development begins.

Testing Philosophy

A comprehensive testing strategy ensures code quality, prevents regressions, and builds confidence in deployments.

Unit Tests

Test individual functions and components in isolation

Integration Tests

Test how components work together

E2E Tests

Test complete user workflows end-to-end

Testing Pyramid

       /\      E2E Tests (Few)
      /  \     - Complete user flows
     /    \    - Cross-browser testing
    /------\   - Slow but comprehensive
   / Integr \  Integration Tests (Some)
  /  ation  \ - API endpoints
 /   Tests   \ - Component interactions
/------------\ - Database operations
/    Unit     \ Unit Tests (Many)
/    Tests     \ - Functions and utilities
/_______________\ - Fast and focused
The testing pyramid suggests having many fast unit tests, fewer integration tests, and only a few comprehensive end-to-end tests.

Unit Testing

Testing Principles

1

Test Behavior, Not Implementation

Focus on what the code does, not how it does it:
// Good - tests behavior
test('calculates total price with tax', () => {
  const result = calculateTotal([10, 20], 0.1);
  expect(result).toBe(33);
});

// Avoid - tests implementation details
test('calls reduce method', () => {
  const spy = jest.spyOn(Array.prototype, 'reduce');
  calculateTotal([10, 20], 0.1);
  expect(spy).toHaveBeenCalled();
});
2

Keep Tests Independent

Each test should run independently:
describe('User Service', () => {
  // Reset state before each test
  beforeEach(() => {
    jest.clearAllMocks();
    database.clear();
  });
  
  test('creates user', () => { /* ... */ });
  test('updates user', () => { /* ... */ });
});
3

Use Descriptive Test Names

// Good - clear and descriptive
test('returns 400 when email is missing', () => { });
test('allows admin to delete users', () => { });
test('prevents user from accessing unauthorized content', () => { });

// Avoid - vague or unclear
test('test1', () => { });
test('it works', () => { });
test('user test', () => { });
4

Follow AAA Pattern

Structure tests with Arrange, Act, Assert:
test('applies discount to order total', () => {
  // Arrange - set up test data
  const order = { items: [{ price: 100 }] };
  const discount = 0.1;
  
  // Act - execute the code being tested
  const result = applyDiscount(order, discount);
  
  // Assert - verify the result
  expect(result.total).toBe(90);
});

Testing Frameworks

Common Testing Tools

Popular JavaScript testing framework:
// Example Jest test
describe('Math utilities', () => {
  test('adds two numbers', () => {
    expect(add(2, 3)).toBe(5);
  });
  
  test('handles edge cases', () => {
    expect(add(0, 0)).toBe(0);
    expect(add(-1, 1)).toBe(0);
  });
});
Features:
  • Built-in mocking
  • Snapshot testing
  • Code coverage
  • Good error messages

Writing Effective Tests

Testing Functions

// Function to test
function calculateDiscount(price, discountPercent, memberLevel) {
  if (price < 0) throw new Error('Price cannot be negative');
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Invalid discount percentage');
  }
  
  let finalDiscount = discountPercent;
  if (memberLevel === 'gold') finalDiscount += 5;
  if (memberLevel === 'platinum') finalDiscount += 10;
  
  return price * (1 - finalDiscount / 100);
}

// Comprehensive tests
describe('calculateDiscount', () => {
  test('calculates basic discount', () => {
    expect(calculateDiscount(100, 10, 'regular')).toBe(90);
  });
  
  test('applies gold member bonus', () => {
    expect(calculateDiscount(100, 10, 'gold')).toBe(85);
  });
  
  test('applies platinum member bonus', () => {
    expect(calculateDiscount(100, 10, 'platinum')).toBe(80);
  });
  
  test('throws error for negative price', () => {
    expect(() => calculateDiscount(-100, 10, 'regular'))
      .toThrow('Price cannot be negative');
  });
  
  test('throws error for invalid discount', () => {
    expect(() => calculateDiscount(100, 150, 'regular'))
      .toThrow('Invalid discount percentage');
  });
});

Testing Async Code

// Async function to test
async function fetchUser(userId) {
  const response = await api.get(`/users/${userId}`);
  return response.data;
}

// Test async code
describe('fetchUser', () => {
  test('fetches user successfully', async () => {
    // Mock the API call
    api.get = jest.fn().mockResolvedValue({
      data: { id: '1', name: 'John' }
    });
    
    const user = await fetchUser('1');
    
    expect(user).toEqual({ id: '1', name: 'John' });
    expect(api.get).toHaveBeenCalledWith('/users/1');
  });
  
  test('handles errors', async () => {
    api.get = jest.fn().mockRejectedValue(new Error('Network error'));
    
    await expect(fetchUser('1')).rejects.toThrow('Network error');
  });
});

Mocking and Stubbing

Why Mock?

  • Isolate the code being tested
  • Avoid slow operations (API calls, database queries)
  • Test error conditions
  • Make tests deterministic

Common Mocking Techniques

// Create a mock function
const mockCallback = jest.fn();

// Use it
[1, 2, 3].forEach(mockCallback);

// Verify it was called
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith(1);
// Mock entire module
jest.mock('./api', () => ({
  getUser: jest.fn(),
  updateUser: jest.fn()
}));

// Import and use mocked module
import { getUser } from './api';

test('uses API', async () => {
  getUser.mockResolvedValue({ id: '1', name: 'Test' });
  
  const result = await someFunction();
  
  expect(getUser).toHaveBeenCalled();
});
const mock = jest.fn();

// Return specific value
mock.mockReturnValue(42);
expect(mock()).toBe(42);

// Return different values on consecutive calls
mock
  .mockReturnValueOnce(1)
  .mockReturnValueOnce(2)
  .mockReturnValue(3);
  
expect(mock()).toBe(1);
expect(mock()).toBe(2);
expect(mock()).toBe(3);
const mock = jest.fn((x) => x * 2);

expect(mock(5)).toBe(10);

// Change implementation
mock.mockImplementation((x) => x + 1);

expect(mock(5)).toBe(6);

Integration Testing

Integration tests verify that multiple components work correctly together.

API Integration Tests

// Example API integration test
describe('User API', () => {
  beforeAll(async () => {
    // Set up test database
    await setupTestDatabase();
  });
  
  afterAll(async () => {
    // Clean up
    await teardownTestDatabase();
  });
  
  beforeEach(async () => {
    // Clear data before each test
    await clearUsers();
  });
  
  test('creates and retrieves user', async () => {
    // Create user
    const createResponse = await request(app)
      .post('/api/users')
      .send({ name: 'Test User', email: '[email protected]' });
    
    expect(createResponse.status).toBe(201);
    const userId = createResponse.body.id;
    
    // Retrieve user
    const getResponse = await request(app)
      .get(`/api/users/${userId}`);
    
    expect(getResponse.status).toBe(200);
    expect(getResponse.body.name).toBe('Test User');
  });
  
  test('validates required fields', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Test' }); // Missing email
    
    expect(response.status).toBe(400);
    expect(response.body.errors).toContain('email');
  });
});

End-to-End Testing

E2E tests simulate real user interactions.

E2E Testing Tools

Developer-friendly E2E testing:
describe('Login Flow', () => {
  it('allows user to login', () => {
    cy.visit('/login');
    
    cy.get('[data-testid="email"]').type('[email protected]');
    cy.get('[data-testid="password"]').type('password123');
    cy.get('[data-testid="login-btn"]').click();
    
    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="welcome"]').should('contain', 'Welcome');
  });
  
  it('shows error for invalid credentials', () => {
    cy.visit('/login');
    
    cy.get('[data-testid="email"]').type('[email protected]');
    cy.get('[data-testid="password"]').type('wrong');
    cy.get('[data-testid="login-btn"]').click();
    
    cy.get('[data-testid="error"]').should('be.visible');
  });
});

Test Coverage

Understanding Coverage

Statement Coverage

Percentage of code statements executed during tests

Branch Coverage

Percentage of conditional branches tested

Function Coverage

Percentage of functions called during tests

Line Coverage

Percentage of code lines executed
Code coverage is a useful metric, but 100% coverage doesn’t guarantee bug-free code. Focus on meaningful tests, not just coverage numbers.

Viewing Coverage

# Generate coverage report (example with Jest)
npm test -- --coverage

# View HTML report
open coverage/index.html

Testing Best Practices

test('handles empty array', () => {
  expect(sum([])).toBe(0);
});

test('handles null input', () => {
  expect(processData(null)).toBeNull();
});

test('handles very large numbers', () => {
  expect(add(Number.MAX_SAFE_INTEGER, 1)).toBeDefined();
});
// Factory for creating test data
function createTestUser(overrides = {}) {
  return {
    id: '123',
    name: 'Test User',
    email: '[email protected]',
    role: 'user',
    ...overrides
  };
}

// Use in tests
test('admin can delete users', () => {
  const admin = createTestUser({ role: 'admin' });
  const result = canDeleteUser(admin);
  expect(result).toBe(true);
});
describe('Database operations', () => {
  afterEach(async () => {
    // Clean up test data
    await database.clear();
  });
  
  test('creates record', async () => {
    await database.create({ name: 'Test' });
    // Test will clean up automatically
  });
});
// Good - focused test
test('validates email format', () => {
  expect(isValidEmail('[email protected]')).toBe(true);
});

test('rejects invalid email', () => {
  expect(isValidEmail('invalid')).toBe(false);
});

// Avoid - testing too much
test('user validation', () => {
  // Tests email, password, name, etc. all in one test
});

Test-Driven Development (TDD)

TDD Workflow

1

Write a Failing Test

Write a test for the functionality you want to add:
test('formats phone number', () => {
  expect(formatPhoneNumber('1234567890'))
    .toBe('(123) 456-7890');
});
// This test will fail because formatPhoneNumber doesn't exist yet
2

Write Minimal Code

Write just enough code to make the test pass:
function formatPhoneNumber(number) {
  return `(${number.slice(0, 3)}) ${number.slice(3, 6)}-${number.slice(6)}`;
}
3

Refactor

Improve the code while keeping tests green:
function formatPhoneNumber(number) {
  // Add validation
  if (number.length !== 10) {
    throw new Error('Invalid phone number length');
  }
  
  return `(${number.slice(0, 3)}) ${number.slice(3, 6)}-${number.slice(6)}`;
}
4

Repeat

Continue the cycle for new functionality

Debugging Tests

test('debug test', () => {
  const data = processData(input);
  
  // Debug output
  console.log('Processed data:', data);
  
  expect(data).toBeDefined();
});

When Development Begins

When ProyectoWeb moves to implementation:
1

Choose Testing Framework

Select appropriate testing tools based on the technology stack
2

Set Up Testing Environment

Configure testing framework, coverage tools, and CI integration
3

Write Tests Alongside Code

Adopt test-driven development or write tests as you code
4

Run Tests Regularly

Run tests locally and in CI/CD pipeline before merging code

Next Steps

Development Guidelines

Review coding standards and best practices

Setup Guide

Learn about development environment setup
Testing is an investment in code quality and maintainability. Start with tests for critical functionality and expand coverage over time.

Build docs developers (and LLMs) love