Testing Your Code

The Course
•
5 min read

A test compares observed behavior with an expectation. It gives you a repeatable way to detect a regression: something that used to work stops working after a change. A passing example does not prove every input works, so choose cases deliberately.

We will use a tiny assertEqual function. If actual and expected differ, it throws an Error. Otherwise it stays quiet. The final message appears only if all checks reached it successfully. This helper uses strict equality, so it is intended for individual values such as numbers, strings, and booleans, not comparing the contents of two separate arrays.

Test an ordinary case, an empty case, and a boundary. For a duration rule, 0, 1, 240, and 241 matter. Include cases you expect to reject; otherwise accepting everything could appear correct. Make one deliberate bug to prove your checks can fail, then restore the working code.

Predict before running

How many assertions run before the success message? What changes if you accidentally replace >= with >?

Predict, then run JavaScript
function assertEqual(actual, expected, label) {
  if (actual !== expected) {
    throw new Error(`${label}: expected ${expected}, got ${actual}`);
  }
}

function reached(minutes, goal) {
  return minutes >= goal;
}

assertEqual(reached(30, 30), true, "exact goal");
assertEqual(reached(29, 30), false, "below goal");
console.log("2 checks passed");
Check your prediction
2 checks passed

Trace each statement in order. If your prediction differed, describe the first place your mental model and the result diverged before trying again.

Tests should challenge the implementation

A test that copies the calculation under test can repeat its mistake. Write an expected answer by hand for a small input. Include an empty case, a normal case, and a boundary. A passing check only covers its assertions; it is evidence, not a proof that every input works.

Tests should challenge the implementation JavaScript
function total(values) {
  let result = 0;

  for (const value of values) {
    result += value;
  }

  return result;
}

const cases = [
  { values: [], expected: 0 },
  { values: [7], expected: 7 },
  { values: [10, 20], expected: 30 }
];

for (const testCase of cases) {
  if (total(testCase.values) !== testCase.expected) {
    throw new Error("Wrong total");
  }
}

console.log("Empty, single, and multiple cases passed");
Check your prediction
Empty, single, and multiple cases passed

Each testCase is an ordinary record with an input array and a hand-written expected total. Temporarily change result += value to result = value. Which check catches the mistake? Restore it afterward.

Choose tests that can distinguish the bug

Testing only a typical value can miss the exact comparison you got wrong. If a goal includes the endpoint, one test must use that endpoint. Testing only 40 against a goal of 30 cannot distinguish > from >=; both return true.

A useful test states an input and an independently chosen expected result. Avoid calculating expected with the same expression as the implementation. For collections, also check whether the function changed its input, because a correct returned value can hide an unwanted side effect.

The boundary separates two implementations JavaScript
function inclusive(value, goal) {
  return value >= goal;
}

function wrong(value, goal) {
  return value > goal;
}

console.log(inclusive(40, 30), wrong(40, 30));
console.log(inclusive(30, 30), wrong(30, 30));
Check your prediction
true true
true false

The first case cannot expose the defect. The second can. Pick below, exactly-at and above the threshold so your test family explains the boundary rather than merely increasing the number of assertions.

Assert the original as well as the result JavaScript
const before = [10, 20];
const after = [...before, 30];

if (after.length !== 3) {
  throw new Error("Missing new item");
}

if (before.length !== 2) {
  throw new Error("Input changed");
}

console.log("result and input checked");
Check your prediction
result and input checked

An implementation that pushes directly into before could return three items and pass the first assertion. The second assertion protects the nonmutation contract. Each check should catch a different possible defect.

Try the rule on new inputs

For a 1–240 input rule, name cases just outside and exactly on both boundaries. Deliberately change one comparison and identify the first test that must fail. Restore the implementation after proving the test matters.

Your turn

Exercise

Testing Your Code: practise

Write isLong(minutes), true at 45 minutes or above. Check 44, 45, and 46 with the helper below. Print 3 checks passed only after all checks.

Your working space JavaScript
function assertEqual(actual, expected, label) {
  if (actual !== expected) {
    throw new Error(label);
  }
}
// Add isLong and three checks.
Hint 1: the idea

The exact boundary is the most valuable case here.

Hint 2: a first step

Return minutes >= 45, then compare each call against its required boolean.

Show one solution
function assertEqual(actual, expected, label) {
  if (actual !== expected) {
    throw new Error(label);
  }
}

function isLong(minutes) {
  return minutes >= 45;
}

assertEqual(isLong(44), false, "below");
assertEqual(isLong(45), true, "boundary");
assertEqual(isLong(46), true, "above");
console.log("3 checks passed");
3 checks passed

Changing >= to > must make the boundary test fail. That deliberate experiment verifies that your check detects a particular defect; it does not establish complete coverage.

What you should be able to explain

  • Why is an empty input useful?
  • Can you make a deliberate defect that your tests catch?
  • What does a passing test not tell you?

Was this article helpful?

Your response is saved on this device.