JavaScript Testing: Getting Started

· 4 min read · Updated March 11, 2026 · beginner
testing jest vitest unit-testing beginner

Testing is an essential skill for any JavaScript developer. Whether you’re building a simple website or a complex web application, tests ensure your code works correctly and continues to work as you make changes. This guide introduces you to the fundamentals of testing in JavaScript.

Why Testing Matters

When you write code, you probably test it manually—refreshing the page, clicking buttons, checking console outputs. Manual testing works for small projects but becomes impractical as your application grows. Tests automate this process, running hundreds of checks in seconds.

Good tests catch bugs before they reach production, document how your code behaves, and give you confidence when refactoring. When you have comprehensive tests, you can change implementation details without fear of breaking functionality.

Consider a simple function that calculates a discount:

function calculateDiscount(price, discountPercent) {
  return price - (price * discountPercent / 100);
}

Without tests, you’d manually verify different inputs. With tests, you write assertions once and run them automatically:

test('calculates 10% discount correctly', () => {
  expect(calculateDiscount(100, 10)).toBe(90);
});

test('returns full price when discount is 0', () => {
  expect(calculateDiscount(50, 0)).toBe(50);
});

Types of Tests

JavaScript tests generally fall into three categories based on scope.

Unit tests verify individual functions or components in isolation. They mock external dependencies like APIs or databases. Unit tests are fast and numerous—you might have hundreds covering a single module.

Integration tests check how multiple units work together. An integration test might verify that a form submission triggers the correct API call and updates the UI.

End-to-end (E2E) tests simulate real user interactions in a browser. Tools like Playwright or Cypress click through your application exactly like a human would. E2E tests are slower but catch issues unit tests miss.

Most projects benefit from a testing pyramid: many unit tests at the base, fewer integration tests in the middle, and few E2E tests at the top.

Your First Test with Jest

Jest is the most popular testing framework for JavaScript. Created by Meta (formerly Facebook), it ships with zero configuration and includes built-in mocking, coverage reporting, and parallel execution.

Install Jest in your project:

npm install --save-dev jest

Add a test script to your package.json:

{
  "scripts": {
    "test": "jest"
  }
}

Now create a test file. Jest automatically finds files matching *.test.js or *.spec.js:

// math.test.js
const { calculateDiscount } = require('./math');

describe('calculateDiscount', () => {
  test('applies percentage discount correctly', () => {
    expect(calculateDiscount(200, 25)).toBe(150);
  });

  test('handles 100% discount', () => {
    expect(calculateDiscount(99, 100)).toBe(0);
  });

  test('throws error for negative discount', () => {
    expect(() => calculateDiscount(100, -10)).toThrow();
  });
});

Run your tests:

npm test

Jest outputs a clean summary showing which tests passed and which failed.

Your First Test with Vitest

Vitest is a modern alternative to Jest, built by the Vite team. It shares Jest’s API but offers faster startup times and native Vite integration. Many new projects choose Vitest for its improved developer experience.

Install Vitest:

npm install --save-dev vitest

Add test scripts:

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run"
  }
}

Write the same test using Vitest:

// math.test.js
import { describe, test, expect } from 'vitest';
import { calculateDiscount } from './math.js';

describe('calculateDiscount', () => {
  test('applies percentage discount correctly', () => {
    expect(calculateDiscount(200, 25)).toBe(150);
  });
});

Run with npm test for watch mode during development, or npm run test:run for a single execution.

Writing Good Tests

Effective tests share several characteristics.

Test behavior, not implementation. Your tests should verify what your code does, not how it does it. This allows refactoring without breaking tests.

// Good: tests the outcome
test('sorts users by name', () => {
  const result = sortUsers(users);
  expect(result[0].name).toBe('Alice');
});

// Avoid: tests implementation details
test('uses quicksort algorithm', () => {
  // This breaks if you switch to mergesort
});

Use descriptive names. Your test names should explain what behavior you’re verifying.

// Clear name explains the requirement
test('returns empty array when input is empty', () => {});

// Vague name requires reading the test body
test('handles edge case', () => {});

Follow the Arrange-Act-Assert pattern. Set up test data, perform the action, then verify the result.

test('adds item to cart', () => {
  // Arrange
  const cart = new Cart();
  const item = { id: 1, name: 'Book', price: 15 };

  // Act
  cart.add(item);

  // Assert
  expect(cart.items).toHaveLength(1);
  expect(cart.items[0]).toEqual(item);
});

Test edge cases. Don’t just test the happy path—verify your code handles null values, empty arrays, and invalid inputs gracefully.

What Comes Next

This introduction covers why testing matters and how to write basic unit tests. The rest of this series dives deeper into specific testing scenarios.

The next tutorial explores unit testing with Jest in detail, covering matchers, mocking, and test organization. Later tutorials cover Vitest, testing asynchronous code, mocking dependencies, DOM testing with jsdom, and end-to-end testing with Playwright.

Testing is a skill that improves with practice. Start writing tests for new code, and gradually add tests to existing codebases. Your future self will thank you.