Software Testing Guide 2026

πŸ“… May 17, 2026‒⏱️ 18 min readβ€’πŸ·οΈ Testing, Quality Assurance

Learn software testing from basics to advanced. Master unit testing, integration testing, E2E testing with modern tools and best practices.

πŸ§ͺ Testing Pyramid

Unit Tests (70%)

Fast, isolated, test individual functions

Integration Tests (20%)

Test component interactions

E2E Tests (10%)

Test complete user workflows

βœ… Unit Testing with Jest/Vitest

// sum.js
export function sum(a, b) {
  return a + b
}

// sum.test.js
import { describe, it, expect } from 'vitest'
import { sum } from './sum'

describe('sum function', () => {
  it('should add two numbers correctly', () => {
    expect(sum(2, 3)).toBe(5)
  })

  it('should handle negative numbers', () => {
    expect(sum(-1, 1)).toBe(0)
  })

  it('should handle zero', () => {
    expect(sum(0, 5)).toBe(5)
  })
})

// Run tests
npm test

Mocking

import { vi } from 'vitest'

// Mock function
const mockFn = vi.fn()
mockFn('hello')
expect(mockFn).toHaveBeenCalledWith('hello')

// Mock module
vi.mock('./api', () => ({
  fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: 'John' }))
}))

// Mock fetch
global.fetch = vi.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ data: 'test' })
  })
)

πŸ”— Integration Testing

// API Integration Test
import request from 'supertest'
import app from './app'

describe('POST /api/users', () => {
  it('should create a new user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John Doe',
        email: 'john@example.com'
      })
      .expect(201)

    expect(response.body).toHaveProperty('id')
    expect(response.body.name).toBe('John Doe')
  })

  it('should return 400 for invalid email', async () => {
    await request(app)
      .post('/api/users')
      .send({
        name: 'John',
        email: 'invalid-email'
      })
      .expect(400)
  })
})

🌐 E2E Testing with Playwright

// tests/login.spec.js
import { test, expect } from '@playwright/test'

test('user can login successfully', async ({ page }) => {
  // Navigate to login page
  await page.goto('http://localhost:3000/login')

  // Fill in form
  await page.fill('input[name="email"]', 'user@example.com')
  await page.fill('input[name="password"]', 'password123')

  // Click login button
  await page.click('button[type="submit"]')

  // Wait for navigation
  await page.waitForURL('**/dashboard')

  // Verify user is logged in
  await expect(page.locator('h1')).toContainText('Dashboard')
  await expect(page.locator('.user-name')).toContainText('John Doe')
})

test('shows error for invalid credentials', async ({ page }) => {
  await page.goto('http://localhost:3000/login')

  await page.fill('input[name="email"]', 'wrong@example.com')
  await page.fill('input[name="password"]', 'wrongpass')
  await page.click('button[type="submit"]')

  await expect(page.locator('.error-message')).toContainText('Invalid credentials')
})

// Run tests
npx playwright test

βš›οΈ React Component Testing

import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import Button from './Button'

describe('Button component', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('calls onClick when clicked', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Click</Button>)
    
    fireEvent.click(screen.getByText('Click'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Click</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
  })
})

🎯 Test-Driven Development (TDD)

  1. Red: Write a failing test
  2. Green: Write minimal code to pass
  3. Refactor: Improve code while keeping tests green
// 1. Red - Write failing test
test('should validate email format', () => {
  expect(isValidEmail('test@example.com')).toBe(true)
  expect(isValidEmail('invalid')).toBe(false)
})

// 2. Green - Implement function
function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}

// 3. Refactor - Improve if needed
function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(email)
}

πŸ“Š Code Coverage

// package.json
{
  "scripts": {
    "test": "vitest",
    "test:coverage": "vitest --coverage"
  }
}

// Run with coverage
npm run test:coverage

// Coverage Report
File      | % Stmts | % Branch | % Funcs | % Lines
----------|---------|----------|---------|--------
All files |   85.5  |   78.2   |   90.1  |   84.8
utils.js  |   92.3  |   85.7   |   100   |   91.2
api.js    |   78.9  |   70.5   |   80.0  |   78.3

// Aim for:
β€’ 80%+ overall coverage
β€’ 100% for critical paths
β€’ Don't chase 100% blindly

βœ… Testing Best Practices

  • Test behavior, not implementation: Focus on what, not how
  • One assertion per test: Keep tests focused
  • Descriptive test names: "should do X when Y"
  • Arrange-Act-Assert: Clear test structure
  • Independent tests: No shared state
  • Fast tests: Unit tests should be milliseconds
  • Mock external dependencies: APIs, databases
  • Test edge cases: Empty, null, boundary values

πŸ“‹ Testing Checklist

  • ☐ Unit tests for business logic
  • ☐ Integration tests for APIs
  • ☐ E2E tests for critical flows
  • ☐ Test edge cases and errors
  • ☐ Mock external dependencies
  • ☐ 80%+ code coverage
  • ☐ Tests run in CI/CD
  • ☐ Fast test execution
  • ☐ Clear test names
  • ☐ No flaky tests

🎯 Conclusion

Testing is not optionalβ€”it's essential for reliable software. Follow the testing pyramid, write tests first (TDD), and make testing part of your development workflow. Good tests give you confidence to refactor and ship faster.

πŸ”§ Test Your Code

Use our developer tools to test and validate your code, APIs, and data formats.

Explore Tools β†’