Course topics

By WebNest Studio Engineering Team

JavaScript Tutorial

Testing Fundamentals in JavaScript

Automated tests are code that checks whether other code behaves correctly, run automatically instead of manually clicking through an application after every change. They catch regressions early, document expected behavior, and give developers confidence to refactor without fear of silently breaking something.

Modern JavaScript testing typically uses a test runner and assertion library like Jest or Vitest, both of which share a very similar API and philosophy.

Why it matters

A change that fixes one thing often breaks another, and without tests nobody notices until a user does. Automated tests check the behaviour of code in seconds, every time it changes. Teams expect code to come with tests, and writing testable functions makes the code itself simpler.

The Arrange-Act-Assert Pattern

Most well-written tests follow a simple three-part structure, regardless of the testing library used.

  • Arrange — set up the inputs, initial state, or objects the test needs.
  • Act — call the function or perform the action being tested.
  • Assert — check that the actual result matches the expected result.

Writing a Simple Test

In Jest or Vitest, tests are grouped in describe() blocks and individual cases are written with test() (or its alias it()). Inside a test, expect(actualValue) returns a set of "matcher" methods like .toBe(), .toEqual(), or .toThrow() to assert the expected outcome. A test file is typically run automatically by the test runner whenever code changes, or as part of continuous integration before code is merged.

What Makes a Good Unit Test

A good unit test is isolated (it tests one function or unit of behavior, not the whole application), deterministic (it produces the same result every run, with no reliance on real time, network calls, or random values unless explicitly controlled), and readable (its name and structure make clear what behavior is being verified and why it might fail).

Examples

A simple function and a Jest/Vitest-style test using Arrange-Act-Assert

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

// sum.test.js
import { sum } from './sum.js';

describe('sum()', () => {
  test('adds two positive numbers correctly', () => {
    // Arrange
    const a = 2;
    const b = 3;

    // Act
    const result = sum(a, b);

    // Assert
    expect(result).toBe(5);
  });

  test('adds negative numbers correctly', () => {
    expect(sum(-2, -3)).toBe(-5);
  });
});

// Running "npx vitest" (or "npx jest") would report:
Output
PASS  sum.test.js
  sum()
    ✓ adds two positive numbers correctly
    ✓ adds negative numbers correctly

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

Testing that a function throws an error under invalid input

JavaScript
// validateAge.js
export function validateAge(age) {
  if (age < 0) {
    throw new Error('Age cannot be negative');
  }
  return age;
}

// validateAge.test.js
import { validateAge } from './validateAge.js';

test('throws an error for a negative age', () => {
  expect(() => validateAge(-1)).toThrow('Age cannot be negative');
});

test('returns the age when valid', () => {
  expect(validateAge(28)).toBe(28);
});
Output
PASS  validateAge.test.js
  ✓ throws an error for a negative age
  ✓ returns the age when valid

Tests: 2 passed, 2 total

Common Mistakes

  • Writing tests that depend on execution order or shared mutable state between tests, causing failures that only appear depending on which tests ran first.
  • Testing implementation details (internal variable names, private helper calls) instead of observable behavior, which makes tests break on harmless refactors.
  • Forgetting to test the "unhappy path" — invalid input, thrown errors, edge cases like empty arrays or zero — and only covering the obvious success case.
  • Wrapping expect(fn()) around a call expected to throw, instead of expect(() => fn()).toThrow() — calling the throwing function outside the assertion crashes the test itself rather than being caught by the matcher.

Exercise

A tiny test runner is provided: test runs a function and prints PASS or FAIL, and expectEqual throws when two values differ. Write isEven, and two tests for it that follow the arrange, act, assert pattern.

Expected output: PASS isEven returns true for 4 then PASS isEven returns false for 7

JavaScript · starter code
function test(name, fn) {
  try {
    fn();
    console.log("PASS " + name);
  } catch (error) {
    console.log("FAIL " + name + ": " + error.message);
  }
}

function expectEqual(actual, expected) {
  if (actual !== expected) {
    throw new Error("expected " + expected + " but got " + actual);
  }
}

function isEven(number) {
  // TODO
}

// TODO: test "isEven returns true for 4"

// TODO: test "isEven returns false for 7"
Show hints
  • A number is even when number % 2 === 0.
  • Each test calls the function with an input and passes the result to expectEqual with the expected value.
Show solution
JavaScript · solution
function test(name, fn) {
  try {
    fn();
    console.log("PASS " + name);
  } catch (error) {
    console.log("FAIL " + name + ": " + error.message);
  }
}

function expectEqual(actual, expected) {
  if (actual !== expected) {
    throw new Error("expected " + expected + " but got " + actual);
  }
}

function isEven(number) {
  return number % 2 === 0;
}

test("isEven returns true for 4", () => {
  const input = 4;
  const result = isEven(input);
  expectEqual(result, true);
});

test("isEven returns false for 7", () => {
  const input = 7;
  const result = isEven(input);
  expectEqual(result, false);
});

Quick quiz

1. What are the three steps of the Arrange-Act-Assert pattern?

  • A. Write, run, deploy
  • B. Import, export, test
  • C. Set up the inputs, call the code under test, check the result
  • D. Plan, code, review
Show answer

C. Keeping the three steps distinct makes a test easy to read.

2. What does a unit test check?

  • A. The visual design
  • B. The whole application running in a browser
  • C. The speed of the server
  • D. One small piece of code, such as a function, in isolation
Show answer

D. Tests of several parts working together are integration tests; tests of full user journeys are end-to-end tests.

3. Which is a sign of a good unit test?

  • A. It depends on the test that ran before it
  • B. It needs a network connection
  • C. It is fast, independent of other tests and gives the same result every time
  • D. It checks many unrelated things at once
Show answer

C. A test that sometimes passes and sometimes fails cannot be trusted.

Interview questions

What is the difference between unit, integration and end-to-end tests?

Show answer

A unit test checks one function or component in isolation; it is fast and pinpoints failures. An integration test checks that several units work together, such as a component with its API layer. An end-to-end test drives the whole application through a real browser as a user would; it gives the most confidence but is the slowest and the most fragile. A healthy test suite has many unit tests, fewer integration tests and a small number of end-to-end tests.

What is a mock and when is it used?

Show answer

A mock is a stand-in for a real dependency, such as an API client, a database or a timer, that returns controlled values and records how it was called. It lets a unit test run quickly and predictably without the network, and makes it possible to simulate failures. Over-mocking is a risk: a test that mocks everything can pass while the real pieces do not fit together.

Key Points to Remember

  • Arrange-Act-Assert is the standard structure for a clear, readable test: set up input, perform the action, check the result.
  • Jest and Vitest share a similar API: describe() groups tests, test()/it() defines one, and expect().toBe() (and other matchers) asserts outcomes.
  • Good unit tests are isolated, deterministic, and readable enough to explain what behavior would break if they failed.
  • Cover both the expected ("happy path") behavior and error/edge cases, not just the simplest successful scenario.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of your HTML, CSS and JavaScript. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering