What you'll be able to do

By the end of this page you should be able to write small tests for pure functions, use the arrange/act/assert shape, and know what a test runner adds later — without drowning in config.

Previous post: security basics for frontend.

Who this is for

  • People who break old features when adding new ones
  • Beginners curious what "unit test" means for a calculator-style function
  • Anyone intimidated by Jest config before writing one assert

You can skip this if you already write tests daily. This closes Phase 11 with the habit, not a full CI course.

Why test a function

A test is a tiny program that calls your function with known inputs and checks outputs.

Scenario A — add(2, 3) must stay 5 after a refactor.

Scenario B — discount rules change; tests list the cases you refuse to forget.

Arrange, act, assert

function add(a, b) {
  return a + b;
}

function assertEqual(actual, expected, label) {
  if (actual !== expected) {
    throw new Error(label + ": expected " + expected + " got " + actual);
  }
}

// arrange
const a = 2;
const b = 3;
// act
const result = add(a, b);
// assert
assertEqual(result, 5, "add 2+3");
console.log("ok");

Scenario A — pure helpers are the easiest first tests.

Scenario B — DOM-heavy code needs more setup; start with pure logic extracted from the UI.

What a runner gives you later

Tools like Node's test runner or Jest watch files, report many cases, and integrate with CI. The idea is the same: call code, compare results, fail loudly.

Scenario A — learning. Hand asserts in a file are enough to feel the habit.

Scenario B — team project. Adopt the repo's runner; do not invent a second one.

A tiny practice file

<!DOCTYPE html>
<html>
  <body>
    <pre id="out"></pre>
    <script>
      function clamp(n, min, max) {
        if (n < min) return min;
        if (n > max) return max;
        return n;
      }
      function assertEqual(actual, expected, label) {
        if (actual !== expected) {
          throw new Error(label + ": expected " + expected + " got " + actual);
        }
      }
      try {
        assertEqual(clamp(5, 0, 10), 5, "mid");
        assertEqual(clamp(-1, 0, 10), 0, "low");
        assertEqual(clamp(99, 0, 10), 10, "high");
        document.getElementById("out").textContent = "all tests passed";
      } catch (err) {
        document.getElementById("out").textContent = String(err.message);
      }
    </script>
  </body>
</html>

Break clamp on purpose and watch the message.

Mistakes I see a lot

1. Testing implementation details ("was this private helper called?") instead of results.

2. One giant test that asserts ten unrelated things — hard to diagnose.

3. No failing test first when fixing a bug — add the case, see red, then fix.

4. Skipping tests for "simple" money/date logic — that is where silent bugs hide.

What to try before the next post

  1. Write three asserts for a function you already have.
  2. Fix a bug by adding a failing assert first.
  3. Skim Node's test docs when you bridge to Node later — same habit, better runner.

This closes Phase 11 — engineering habits. Next we begin Phase 12 — practice: coding problems warmup.

Try this next outside the series

Engineering habits only stick when they show up in testing, security, and maintenance work.