Skip to content

Expert · Testing and API documentation · topic 44 of 54 · 25 min

Unit testing: checking one piece at a time

Explanation

A test is a small piece of code that runs your code and checks the answer. A unit test checks one small piece, usually one function, with no network and no database, so it runs in milliseconds. Good tests follow Arrange, Act, Assert, have names that explain the rule, and cover the edge cases like empty, zero and negative values.

Why write tests at all?

When you change code, you can break something that used to work. Checking every feature by hand after every change is slow, and people forget steps. A test is code that runs your code with a known input and checks that the output is what you expect. A computer can run hundreds of tests in a few seconds, every time you save.

Tests give you three things. First, they catch bugs before users see them. Second, they let you change and clean up code without fear, because the tests tell you if you broke a rule. Third, they explain how the code should behave: a test named "free delivery above 499 rupees" is documentation that never goes out of date.

Think of it like this: Think of a kirana shop owner who counts the cash box every night and compares it with the bill book. If the numbers match, the day was fine. If they do not, he knows something went wrong today, while he can still remember what happened. A test suite is that nightly count for your code: quick, regular, and it catches mistakes while they are fresh.

What is a unit test?

A unit is the smallest piece of code that does one job, usually one function. A unit test calls that function directly with some input and checks the result. It does not start a server, call the internet or touch a real database. Because of that it is fast (milliseconds) and it always gives the same answer.

Arrange, Act, Assert

Most good tests have three parts. Arrange: prepare the input and anything the code needs. Act: call the function once. Assert: check the result. An assertion is a line that says "this must be true", for example "the total must be 165". If it is not true, the test fails and shows you both values. A test runner is the program that finds your test files, runs them and prints which passed and which failed.

javascript
import { describe, it, expect } from 'vitest';
import { cartTotal } from './cart.js';

describe('cartTotal', () => {
  it('adds price times quantity for every item', () => {
    // Arrange
    const items = [{ price: 60, qty: 2 }, { price: 45, qty: 1 }];
    // Act
    const total = cartTotal(items);
    // Assert
    expect(total).toBe(165);
  });

  it('returns 0 for an empty cart', () => {
    expect(cartTotal([])).toBe(0);
  });

  it('throws when a quantity is negative', () => {
    expect(() => cartTotal([{ price: 60, qty: -1 }])).toThrow();
  });
});
cart.test.js with Vitest, run on your computer with npx vitest. Each it() is one test.

Look at the test names. "adds price times quantity for every item" tells you the rule. When it fails, the name alone tells you what broke. Names like "test1" or "works" tell you nothing. A good pattern is: what it does, and when. For example "returns 404 when the note does not exist".

Edge cases: where bugs hide

  • Empty: an empty cart, an empty string, an empty list of users.
  • Zero: a price of 0, a quantity of 0, a GST rate of 0.
  • Negative: a refund of -500, a quantity of -1. Should the code accept it or refuse it?
  • Very large: 10 crore rupees, a list of one lakh items, a 5000-character name.
  • Rounding: 199.99 with 18% GST is 35.9982 rupees. Money must be rounded to paise (2 decimal places) in a fixed, tested way.
  • Wrong type or missing value: undefined, null, the text "100" instead of the number 100.
Test runnerLanguageNotes
VitestJavaScript / TypeScriptFast, modern, works well with Vite and TypeScript projects
JestJavaScript / TypeScriptVery common in older and larger projects; almost the same API as Vitest
node:testJavaScriptBuilt into Node.js; no install needed; run with node --test
pytestPythonThe usual choice in Python; tests are plain functions using assert
JUnitJavaThe standard runner in Java and Spring projects
python
# test_cart.py, run with: pytest
from cart import cart_total

def test_adds_price_times_quantity():
    items = [{'price': 60, 'qty': 2}, {'price': 45, 'qty': 1}]
    assert cart_total(items) == 165

def test_empty_cart_costs_nothing():
    assert cart_total([]) == 0
The same idea in Python with pytest. Any function whose name starts with test_ is a test.

Test doubles: stubs, mocks and fakes

Some code needs things that are slow or dangerous in a test: sending a real SMS, charging a real card, reading the current time. A test double is a stand-in object you pass in instead. A stub returns a fixed answer ("the time is always 10:00"). A fake is a small working version (an SMS sender that saves messages in a list instead of sending). A mock also records how it was called, so the test can check "send was called once with this number". To make this possible, the code should receive its helpers as parameters instead of creating them inside. This is called dependency injection.

javascript
// Real code: gets its SMS sender as a parameter
async function sendWelcome(user, sms) {
  await sms.send(user.phone, 'Welcome to the app, ' + user.name);
}

// Test: a fake sender that only records messages
const sent = [];
const fakeSms = { send: async (to, text) => { sent.push({ to, text }); } };
await sendWelcome({ name: 'Asha', phone: '9876543210' }, fakeSms);
console.log(sent); // [{ to: '9876543210', text: 'Welcome to the app, Asha' }]
No real SMS is sent and no money is spent. The test only checks what the code asked the sender to do.
Tip: When you find a bug, first write a test that fails because of it, then fix the code until the test passes. The bug can never come back silently.
Careful: Do not chase 100% coverage (the share of lines that tests run) for its own sake. Test the rules that matter: money, permissions, validation and edge cases. Also avoid mocking everything: a test that only checks which mocks were called can pass while the real feature is broken.

Watch the lesson

Unit testing: checking one piece at a time · 1/7

Unit testing

Every time you change code, you might break something that used to work. Tests catch that for you, in seconds.

Prefer a video by a teacher? Find free videos on “unit testing javascript vitest tutorial for beginners” on YouTube ↗

In real life: GST on a kirana billing app

A small billing app adds GST to every bill. One day a developer changes the rounding code, and bills of 199.99 rupees start showing 35.99 rupees of tax instead of 36.00. Shop owners notice a few paise of difference only weeks later, when their accounts do not match. With unit tests like "rounds 18% GST on 199.99 to 36.00", the mistake would have failed the test the moment the code was saved, before any bill was printed. GST rates can also change, so the function takes the rate as a parameter and the tests check several rates.

Practice (0/3 done)

Task 1 · Code · Javascript

Write calculateGst(amount, rate) that returns the GST in rupees, rounded to paise (2 decimal places). rate is a percent, for example 18. Throw an Error if amount is negative. The tests below cover normal values and edge cases. Hint: amount * rate is the GST in paise, so Math.round(amount * rate) / 100 rounds it safely.

Task 2 · Code · Javascript

Write an async function sendOtp(phone, sms, makeCode). phone must be 10 digits starting with 6, 7, 8 or 9. If it is not valid, return { sent: false, error: "Invalid phone" } and do NOT call sms.send. If it is valid, get a code from makeCode(), call await sms.send(phone, "Your OTP is " + code) and return { sent: true }. The tests pass in a fake sms object and a stub makeCode, so no real SMS is sent.

Task 3 · Practice

A function applyCoupon(cartTotal, coupon) gives "SAVE50" = 50 rupees off on carts of 299 rupees or more, and never makes the total below 0. Write a list of 6 test names (in plain English) with the input and expected output for each. Include edge cases.

Quick quiz

Question 1

1.What does a unit test usually check?

Question 2

2.In Arrange, Act, Assert, what happens in the Act step?

Question 3

3.Which test name is the most useful?

Question 4

4.Your code sends real SMS messages. How should a unit test handle this?

Question 5

5.Which of these is an edge case for a discount function?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Logging and monitoring