9.4 Testing Principles

Aligned outcomes:

SLO 3

Describe, design, implement, and test structured programs using currently accepted methodology.

SLO 4

Explain what an algorithm is and its importance in computer programming.

Learning Objectives

By the end of this section, you will be able to:

In this section, you will learn to:
  • explain why automated testing is better than manual testing;
  • describe the three parts of Behavior Driven Development (BDD);
  • write a spec with describe, it, and assert for a function;
  • follow the development flow of writing a spec, then an implementation;
  • use before/after and beforeEach/afterEach to set up and tear down tests.

9.4.1 Why do we need tests?

When developing functions, manual testing becomes imperfect and error-prone. A developer might test f(1) successfully, fix an issue with f(2), but forget to re-verify that f(1) still works. This scenario is common in development.

Manual testing forgets

When you test by hand, you tend to check the newest thing and forget the old cases. Fixing f(2) can break f(1), and you might never notice until much later.

Automated testing means that tests are written separately, in addition to the code. They run our functions in various ways and compare results with the expected.

A checklist that runs itself

Automated tests are like a checklist that runs every time. Instead of you remembering to re-check f(1), the test suite does it for you, every single time.

Try It Now 9.4.1

Why is automated testing better than manual testing for a function like f?

Solution

Step 1 — recall the manual problem: Manual testing is error-prone because a developer can fix one case and forget to re-check the others.

Step 2 — state the automated advantage: Automated tests run all the cases every time, so fixing one case does not silently break another.

Answer: Automated testing runs every test case on every change, so it catches regressions that manual testing would miss.

9.4.2 Behavior Driven Development (BDD)

BDD combines three elements: tests, documentation, and examples. This approach structures development around specifications that describe what code should do before implementation begins.

Write the promise first

BDD says: decide what the code should do before you write it. That written promise becomes your tests, your documentation, and your examples all at once.

Three jobs, one spec

A single spec does three things: it tests the code, it documents what the code does, and it shows examples of how to use it. That is why BDD is so efficient.

Try It Now 9.4.2

What three elements does BDD combine?

Solution

Step 1 — list the elements: BDD combines tests, documentation, and examples.

Step 2 — explain the idea: A specification describes what the code should do before implementation begins, and that spec serves all three purposes.

Answer: BDD combines tests, documentation, and examples.

9.4.3 Development of "pow": the spec

Consider creating a pow(x, n) function that raises x to an integer power n. A specification describes expected behavior:

describe("pow", function() {
  it("raises to n-th power", function() {
    assert.equal(pow(2, 3), 8);
  });
});
A spec is a promise in code

This spec promises that pow(2, 3) will equal 8. Before we write the function, we write down what it should do.

The spec has three main components:

Read it like a sentence

describe("pow", ...) says "about the pow function", and it("raises to n-th power", ...) says "it raises to the n-th power". Together they read like a plain-English sentence.

Try It Now 9.4.3

What are the three main components of a spec, and what does each one do?

Solution

Step 1 — name the components: The three components are describe, it, and assert.equal.

Step 2 — say what each does: describe groups related tests, it defines an individual test case, and assert.equal checks whether two values match.

Answer: describe groups related tests, it defines individual test cases, and assert.equal checks that values match.

9.4.4 The development flow

Development follows an iterative cycle:

  1. Write initial spec with basic tests
  2. Create initial implementation
  3. Run tests using Mocha framework
  4. Fix code until tests pass
  5. Add more test cases to spec
  6. Update implementation
  7. Repeat until complete
Small steps, then grow

You do not write the whole function at once. You write a small spec, make it pass, then add more cases and grow the implementation. Each step is small and verifiable.

A loop, not a one-shot

The flow is a loop: spec, implement, test, fix, add more, repeat. You keep going around until the function handles every case you care about.

Try It Now 9.4.4

Put the development flow steps in order: update implementation, run tests, write initial spec, fix code until tests pass, add more test cases, create initial implementation.

Solution

Step 1 — start with the spec: Write the initial spec with basic tests.

Step 2 — build the first version: Create the initial implementation.

Step 3 — check it: Run the tests using Mocha.

Step 4 — fix problems: Fix the code until the tests pass.

Step 5 — grow the spec: Add more test cases to the spec.

Step 6 — grow the code: Update the implementation to handle the new cases.

Answer: The order is: write initial spec, create initial implementation, run tests, fix code until tests pass, add more test cases, update implementation.

9.4.5 The spec in action

Three libraries support testing:

Three tools, three jobs

Mocha runs the tests, Chai checks the results, and Sinon helps with advanced cases like spying on functions. Together they form a complete testing setup.

A complete HTML test page looks like:

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
<script>
mocha.setup('bdd');
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
<script>
let assert = chai.assert;
</script>
</head>
<body>
<script>
function pow(x, n) {
  /* function code is to be written, empty now */
}
</script>
<script src="test.js"></script>
<div id="mocha"></div>
<script>
mocha.run();
</script>
</body>
</html>
The page wires everything together

The HTML page loads Mocha and Chai, sets up the test style, defines the function, loads the test file, and then runs the tests. Each <script> tag has one job.

Try It Now 9.4.5

What role does each of Mocha, Chai, and Sinon play in testing?

Solution

Step 1 — Mocha: Mocha is the core framework that provides describe, it, and runs the tests.

Step 2 — Chai: Chai is the assertion library that provides comparison functions like assert.equal.

Step 3 — Sinon: Sinon is a spy and mock library for advanced testing.

Answer: Mocha runs the tests, Chai checks the results, and Sinon provides spies and mocks for advanced testing.

9.4.6 Initial implementation

A simple implementation to pass initial tests:

function pow(x, n) {
  return 8; // :) we cheat!
}
Cheating is a starting point

This implementation always returns 8, which passes the single test pow(2, 3) === 8. It is not a real solution, but it is enough to get the first test green and start the loop.

Green first, correct later

The point of the first implementation is to make the test pass, not to be complete. Once the loop is running, you add more tests that force a real implementation.

Try It Now 9.4.6

Why does the initial implementation return 8 pass the first test, and why is it not a real solution?

Solution

Step 1 — why it passes: The first test checks pow(2, 3) === 8, and the function always returns 8, so the test passes.

Step 2 — why it is not real: The function returns 8 for every input, so it fails for any other case like pow(3, 4).

Answer: It passes because the first test expects 8, but it is not a real solution because it ignores its inputs and returns 8 for everything.

9.4.7 Improving the spec

When tests pass but functionality is incomplete, more test cases must be added. Instead of multiple assertions in one test, separate tests provide clearer failure information:

describe("pow", function() {
  it("2 raised to power 3 is 8", function() {
    assert.equal(pow(2, 3), 8);
  });

  it("3 raised to power 4 is 81", function() {
    assert.equal(pow(3, 4), 81);
  });
});
One test, one thing

When a test fails, you want to know exactly which behavior broke. Splitting assertions into separate tests tells you precisely which case failed.

One test checks one thing. Separating assertions makes debugging easier.

A clear failure message

A test named "3 raised to power 4 is 81" tells you exactly what went wrong when it fails. A single test with many assertions cannot tell you which one broke.

Try It Now 9.4.7

Why is it better to use separate tests instead of multiple assertions in one test?

Solution

Step 1 — recall the rule: One test checks one thing.

Step 2 — explain the benefit: When a test fails, a separate test tells you exactly which case broke, making debugging easier.

Answer: Separate tests give clearer failure information, because each test tells you precisely which behavior failed.

9.4.8 Improving the implementation

A proper implementation that handles multiple cases:

function pow(x, n) {
  let result = 1;
  for (let i = 0; i < n; i++) {
    result *= x;
  }
  return result;
}
A real loop

This implementation multiplies x by itself n times using a loop. It now handles any positive integer power, not just the cases in the tests.

Tests can be generated programmatically:

describe("pow", function() {
  function makeTest(x) {
    let expected = x * x * x;
    it(`${x} in the power 3 is ${expected}`, function() {
      assert.equal(pow(x, 3), expected);
    });
  }

  for (let x = 1; x <= 5; x++) {
    makeTest(x);
  }
});
Write tests with a loop

Instead of typing five similar tests by hand, we use a loop to generate them. The makeTest function builds one test for each value of x from 1 to 5.

Try It Now 9.4.8

What does the loop in the programmatic test example do?

Solution

Step 1 — see the loop: The for loop runs makeTest(x) for each x from 1 to 5.

Step 2 — see what makeTest does: Each call to makeTest(x) creates a test that checks pow(x, 3) equals x x x.

Answer: The loop generates five tests, one for each value of x from 1 to 5, each checking that pow(x, 3) equals x x x.

9.4.9 Nested describe

Related tests can be grouped using nested describe blocks:

describe("pow", function() {
  describe("raises x to power 3", function() {
    function makeTest(x) {
      let expected = x * x * x;
      it(`${x} in the power 3 is ${expected}`, function() {
        assert.equal(pow(x, 3), expected);
      });
    }

    for (let x = 1; x <= 5; x++) {
      makeTest(x);
    }
  });
});
Grouping keeps tests organized

As a test suite grows, nested describe blocks group related tests together. The outer block says "about pow", and the inner block says "about raising to power 3".

Folders inside folders

Nested describe blocks are like folders inside folders. They keep related tests together so a large suite stays readable.

Try It Now 9.4.9

What is the purpose of nesting a describe block inside another describe block?

Solution

Step 1 — state the purpose: Nested describe blocks group related tests together.

Step 2 — give the benefit: Grouping keeps a large test suite organized and readable, with the outer block describing the function and inner blocks describing specific behaviors.

Answer: Nested describe blocks group related tests, keeping a large suite organized and readable.

9.4.10 before/after and beforeEach/afterEach

Setup and teardown functions execute at specific times:

describe("test", function() {
  before(() => console.log("Testing started – before all tests"));
  after(() => console.log("Testing finished – after all tests"));
  beforeEach(() => console.log("Before a test – enter a test"));
  afterEach(() => console.log("After a test – exit a test"));

  it('test 1', () => console.log(1));
  it('test 2', () => console.log(2));
});
Set up and clean up

before and after run once around all the tests, while beforeEach and afterEach run around each individual test. This is how you prepare and clean up test data.

Execution sequence: beforebeforeEach → test → afterEach → (repeat for each test) → after

Once vs. every time

before/after run once for the whole group. beforeEach/afterEach run before and after every single test. Choose based on whether the setup applies to the group or to each test.

Try It Now 9.4.10

For the example with two tests, list the full order in which the console.log calls appear.

Solution

Step 1 — the group setup: before runs first, showing "Testing started".

Step 2 — the first test: beforeEach runs, then test 1 shows 1, then afterEach runs.

Step 3 — the second test: beforeEach runs again, then test 2 shows 2, then afterEach runs again.

Step 4 — the group teardown: after runs last, showing "Testing finished".

Answer: The order is: before, beforeEach, test 1 (1), afterEach, beforeEach, test 2 (2), afterEach, after.

9.4.11 Extending the spec

For invalid inputs, return NaN:

describe("pow", function() {
  it("for negative n the result is NaN", function() {
    assert.isNaN(pow(2, -1));
  });

  it("for non-integer n the result is NaN", function() {
    assert.isNaN(pow(2, 1.5));
  });
});
Define the edge cases

A good spec does not only test normal inputs. It also says what should happen for invalid ones, like a negative power or a non-integer power.

Common assertions from Chai:

A toolbox of checks

Chai gives you many assertion functions. Each one checks a different thing, so you can express exactly what you expect.

Updated implementation:

function pow(x, n) {
  if (n < 0) return NaN;
  if (Math.round(n) != n) return NaN;

  let result = 1;
  for (let i = 0; i < n; i++) {
    result *= x;
  }
  return result;
}
Guard the invalid inputs

The two if statements at the top return NaN for negative or non-integer powers. This makes the function match the spec's promise about invalid inputs.

Try It Now 9.4.11

What does the updated pow implementation return for pow(2, -1) and pow(2, 1.5), and why?

Solution

Step 1 — check the negative case: For pow(2, -1), n < 0 is true, so the function returns NaN.

Step 2 — check the non-integer case: For pow(2, 1.5), Math.round(1.5) != 1.5 is true, so the function returns NaN.

Answer: Both return NaN, because the guards at the top of the function reject negative and non-integer powers.

Summary

BDD places specifications first, followed by implementation. The spec serves three purposes:

  1. Tests — verify correct behavior
  2. Documentation — titles explain functionality
  3. Examples — show practical usage

Well-tested code enables safe refactoring and improves architecture by requiring clear function contracts. In large projects, tests prevent regression bugs and provide confidence during modifications.

Problem Set 9.4

Problem 1. Why is manual testing of functions imperfect and error-prone?

Solution

Step 1 — recall the manual testing scenario: A developer might test f(1) successfully, then fix an issue with f(2), but forget to re-verify that f(1) still works.

Step 2 — explain why this happens: Manual testing relies on human memory and attention. When you test by hand, you tend to check the newest thing and forget the old cases, so a fix for one case can silently break another without anyone noticing until much later.

Answer: Manual testing is imperfect because it depends on the developer remembering to re-check every case; fixing one case (like f(2)) can break an earlier case (like f(1)), and humans often forget to re-verify old cases, so errors slip through unnoticed.

Problem 2. What does automated testing mean?

Solution

Step 1 — state the definition: Automated testing means that tests are written separately, in addition to the code itself.

Step 2 — describe what they do: These tests run our functions in various ways and compare the results with the expected values. They act like a checklist that runs itself on every change, catching regressions automatically.

Answer: Automated testing means writing tests separately from the code; these tests run the functions in various ways and compare results with expected values, every time the code changes.

Problem 3. What three elements does BDD combine?

Solution

Step 1 — list the elements: BDD combines three elements: tests, documentation, and examples.

Step 2 — explain how they come together: A specification describes what the code should do before implementation begins, and that single spec serves all three purposes at once — it tests the code, documents what it does, and shows usage examples.

Answer: BDD combines tests, documentation, and examples around a written specification.

Problem 4. What does describe do in a spec?

Solution

Step 1 — identify its role: describe("title", function() { ... }) is the first component of a spec.

Step 2 — say what it does: It groups related tests together under a title, such as describe("pow", ...), which says "about the pow function". Nested describe blocks can group tests even more finely as the suite grows.

Answer: describe groups related tests under a named block, organizing the spec around the function or behavior being tested.

Problem 5. What does it do in a spec?

Solution

Step 1 — identify its role: it("use case description", function() { ... }) is the second component of a spec.

Step 2 — say what it does: It defines an individual test case with a human-readable description of one specific behavior, such as it("raises to n-th power", ...). Read together with describe, it forms a plain-English sentence about what the code should do.

Answer: it defines an individual test case, described in plain English, that checks one specific behavior of the function.

Problem 6. What does assert.equal(value1, value2) check?

Solution

Step 1 — identify what it compares: assert.equal(value1, value2) takes two values.

Step 2 — say what it checks: It checks whether the two values are equal. If they match, the test passes; if they differ, the test fails and reports the mismatch, telling you exactly which expectation broke.

Answer: assert.equal(value1, value2) checks whether two values are equal — if they are not, the test fails.

Problem 7. List the steps of the development flow in order.

Solution

Step 1 — start with the spec: Write the initial spec with basic tests describing what the function should do.

Step 2 — create the implementation: Write an initial implementation of the function.

Step 3 — run the tests: Run the tests using the Mocha framework.

Step 4 — fix failures: Fix the code until all tests pass.

Step 5 — grow the spec: Add more test cases to cover additional behaviors and edge cases.

Step 6 — grow the implementation: Update the implementation to satisfy the new cases.

Step 7 — repeat: Continue the loop until the function handles every case you care about.

Answer: The order is: write initial spec → create initial implementation → run tests → fix code until tests pass → add more test cases → update implementation → repeat until complete.

Problem 8. What role does Mocha play in testing?

Solution

Step 1 — identify Mocha's job: Mocha is the core testing framework.

Step 2 — list what it provides: It provides describe and it for structuring specs, and it executes the tests, reporting which ones pass and fail.

Answer: Mocha is the core framework that provides describe, it, and runs/executes the tests.

Problem 9. What role does Chai play in testing?

Solution

Step 1 — identify Chai's job: Chai is the assertion library.

Step 2 — list what it provides: It supplies comparison functions such as assert.equal, assert.strictEqual, assert.isTrue, and assert.isNaN, which check actual results against expected ones.

Answer: Chai is the assertion library that provides comparison functions like assert.equal for checking results against expectations.

Problem 10. What role does Sinon play in testing?

Solution

Step 1 — identify Sinon's job: Sinon is the spy and mock library.

Step 2 — say when it is used: It supports advanced testing scenarios, such as spying on functions to see how they were called, or mocking parts of the system.

Answer: Sinon is a spy and mock library used for advanced testing cases.

Problem 11. Why is it better to use separate tests instead of multiple assertions in one test?

Solution

Step 1 — recall the rule: One test should check one thing.

Step 2 — explain the benefit: When assertions are split into separate tests, a failure tells you exactly which behavior broke — for example, a test named "3 raised to power 4 is 81" pinpoints the failing case immediately. A single test with many assertions cannot tell you which one failed.

Answer: Separate tests give clearer failure information, because each failing test identifies precisely which behavior broke, making debugging easier.

Problem 12. What is the purpose of nested describe blocks?

Solution

Step 1 — state the purpose: Nested describe blocks group related tests together within a larger suite.

Step 2 — give the benefit: Like folders inside folders, nesting keeps a large suite organized and readable — the outer block describes the function (e.g., "pow") and inner blocks describe specific behaviors (e.g., "raises x to power 3").

Answer: Nested describe blocks group related tests together, keeping a large test suite organized and readable.

Problem 13. What is the difference between before/after and beforeEach/afterEach?

Solution

Step 1 — describe before/after: before runs once before all tests in the group, and after runs once after all tests finish. Use them for setup/teardown that applies to the whole group.

Step 2 — describe beforeEach/afterEach: beforeEach runs before every individual test, and afterEach runs after every individual test. Use them when each test needs fresh setup or cleanup.

Step 3 — show the sequence: The execution order is: before → (beforeEach → test → afterEach) repeated for each test → after.

Answer: before/after run once around the entire group of tests, while beforeEach/afterEach run around each individual test.

Problem 14. What does assert.isNaN(value) check?

Solution

Step 1 — identify what it checks: assert.isNaN(value) is one of Chai's assertion functions.

Step 2 — say what it does: It checks that the given value is NaN (Not-a-Number). If the value is not NaN, the test fails.

Answer: assert.isNaN(value) checks that the value is NaN.

Problem 15. What does the updated pow implementation return for a negative or non-integer power?

Solution

Step 1 — check the negative case: For a negative power like pow(2, -1), the guard if (n < 0) return NaN; triggers, so the result is NaN.

Step 2 — check the non-integer case: For a non-integer power like pow(2, 1.5), the guard if (Math.round(n) != n) return NaN; triggers, since Math.round(1.5) is 2, which does not equal 1.5, so the result is also NaN.

Answer: The updated pow returns NaN for both negative powers and non-integer powers, matching the spec's promise about invalid inputs.

Key Terms

automated testing — writing tests separately from the code that run the functions and compare results with the expected.

Behavior Driven Development (BDD) — a development approach that combines tests, documentation, and examples around a specification.

spec — a written description of what code should do, used as tests, documentation, and examples.

assertion — a check that compares a value against an expected result.

regression — a bug introduced when a change breaks behavior that previously worked.