munotes®

Testing the Project: the Levels, the Test Plan and the Tools

Get access to whole semester resourcesSemester Pass

Chapter Fifty

Syllabus topic Module 2, "Integration & System Testing", as a whole: the levels of testing, the plan, and the tools.

Pages 344 to 349 of 499

In one line

Testing is not one activity but four, each answering a different question, unit, integration, system and acceptance, written down in advance in a test plan that says what will be tested, how and by whom, and run in this project by one command with no testing library at all.

In the wording to use when asked: software testing is conducted at successive levels: unit testing of individual components in isolation, integration testing of components working together, system testing of the complete system against its specification, and acceptance testing by or on behalf of the user; test design may be black-box, derived from the specification, or white-box, derived from the code's structure, and the test plan records the scope, approach, resources, schedule and the criteria for passing.

The four levels

LevelThe question it answersHereWho
Unitdoes this one piece do what it should, on its own?the rules, the validation, the password functions, the limiterthe person who wrote it
Integrationdo the pieces work together, with the real database?the routes, services and store, through real HTTP requeststhe team
Systemdoes the whole thing do what the SRS says?every requirement, end to end, on the deployed systemthe team, from the SRS
Acceptancewill the people who asked for it use it?the owner and Ganesh, at the canteenthe users

Each level catches what the one below it cannot. A unit test proves canMove('placed', 'ready', 'staff') is false; it cannot prove that the counter's button sends the right status. An integration test proves the route refuses that move; it cannot prove that the counter can see the button during the rush. That is what system and acceptance testing are for (Chapter 54).

The cost of a mistake rises at every level. A unit test fails in a second on a laptop; a fault found in acceptance testing costs a meeting, a change, and another round of everything.

Black-box and white-box

Two ways of deciding what to test, and a project needs both:

  • Black box: tests derived from the specification alone, without looking at the code. "FR-7 allows 1 to 5 of an item" gives tests at 0, 1, 5 and 6, whatever the code looks like. Chapter 52 is the systematic way of doing this.
  • White box: tests derived from the code's own structure, to reach paths the specification does not name: what happens when the second item of an order is short, when the database is down, when the same request arrives twice.

A suite of only black-box tests misses the paths the code invented for itself; a suite of only white-box tests proves the code does what it does, which is not the same as what it should.

munotes.in344

Testing the Project: the Levels, the Test Plan and the Tools

The tools

Node.js has a test runner built into it, node:test, and an assertion library, node:assert. The worked project uses them and nothing else: no Jest, no Mocha, no Chai, no supertest. ADR-1 chose the stack for what the team could explain; a test suite is the last place to add something nobody can (Chapter 26).

A test file reads the same as it would with any of those libraries:

describe('isSlotOpen', () => {
  it('is open one minute before the cut-off', () => {
    assert.equal(rules.isSlotOpen('12:40', at('12:24'), IST), true);
  });
  • describe groups tests; it is one test, named so that the report reads as a sentence.
  • assert.equal compares with ===; assert.deepEqual compares objects and arrays; assert.match tests a regular expression; assert.ok takes anything true.
  • The strict form, node:assert/strict, is what the project imports, so assert.equal(1, '1') fails, as it should.

One command

    "test": "node --test --test-concurrency=1 --test-reporter=./test/reporter.js \"test/**/*.test.js\"",
    "test:unit": "node --test --test-reporter=./test/reporter.js test/unit/*.test.js",

Three details, each of which was a mistake first:

  • The pattern is named. Without test/**/*.test.js, node --test walks the whole project and runs anything that looks like a test, which once meant the load-testing script, which then "failed" for want of a running server.
  • --test-concurrency=1 runs one file at a time, because the integration tests share one database and rebuild it for each file. Tests that fight over one database fail at random, which is worse than failing.
  • A reporter of the project's own. Node's default marks each test with a tick or a cross, which not every terminal can draw; this one prints plain words, so the report is readable anywhere, including in this book.
'use strict';

const path = require('node:path');

// The report `npm test` prints. Node's own reporter marks
// each test with a tick or a cross, which some terminals
// (older Windows ones among them) cannot draw; this one
// prints plain words: each file, each test in it, then the
// totals. A failure also prints why it failed.

module.exports = async function* plainReporter(source) {
  let file = '';
  let passed = 0;
  let failed = 0;
  for await (const { type, data } of source) {
    if (type === 'test:stderr') {
      yield data.message;
      continue;
    }
    if (type !== 'test:pass' && type !== 'test:fail') continue;
    if (data.details.type === 'suite') continue;
    if (data.file && data.file !== file) {
      file = data.file;
      yield `${path.relative(process.cwd(), file)}\n`;
    }
    if (type === 'test:pass') {
      passed += 1;
      yield `  pass  ${data.name}\n`;
    } else {
      failed += 1;
      const err = data.details.error;
      const why = (err.cause && err.cause.message) || err.message;
      yield `  FAIL  ${data.name}\n        ${why}\n`;
    }
  }
  yield `\n${passed + failed} tests: ${passed} passed, `
    + `${failed} failed\n`;
};
munotes.in345

Testing the Project: the Levels, the Test Plan and the Tools

npm test runs everything; npm run test:unit runs only the fast tests, in a second, which is what a developer runs every few minutes while writing code (NFR-11).

The shared helper

Every integration test needs the same three things: a database in a known state, the application running, and a client that keeps its cookie as a browser does. One helper gives all three:

'use strict';

// Shared by the integration tests: a freshly built test
// database, the application started on a free port, and a
// client that keeps its sign-in cookie between requests the
// way a browser does.

const fs = require('node:fs');
const path = require('node:path');
const { loadConfig } = require('../src/config');
const { createPool } = require('../src/db');
const { createStore } = require('../src/store');
const { createApp } = require('../src/app');
const { resetDatabase } = require('../scripts/setup-db');

const envFile = path.join(__dirname, '..', '.env');
if (fs.existsSync(envFile)) process.loadEnvFile(envFile);

// Tests never touch the real database: they use their own.
// A test may change other settings, as the HTTPS test does.
function testConfig(settings = {}) {
  return loadConfig({
    ...process.env,
    DB_NAME: process.env.TEST_DB_NAME || 'canteen_test',
    LOG_REQUESTS: 'false',
    ...settings,
  });
}

// A clock the tests can set. Unless a test moves it, it is
// 10:30 in the morning in Mumbai, when every slot is open.
function testClock(when = '2026-09-29T10:30:00+05:30') {
  let now = new Date(when);
  const clock = () => now;
  clock.set = (value) => {
    now = new Date(value);
  };
  return clock;
}

// Keeps the test output clean, but never hides a real error.
const quietLog = { info() {}, error: console.error };

async function startApp(settings) {
  const config = testConfig(settings);
  await resetDatabase(config.db);
  const pool = createPool(config.db);
  const clock = testClock();
  const app = createApp({
    config, pool, store: createStore(pool), clock, log: quietLog,
  });
  const server = await new Promise((resolve) => {
    const s = app.listen(0, '127.0.0.1', () => resolve(s));
  });
  const base = `http://127.0.0.1:${server.address().port}`;
  return {
    base,
    clock,
    pool,
    client: () => client(base),
    async stop() {
      await new Promise((resolve) => server.close(resolve));
      await pool.end();
    },
  };
}

function client(base) {
  let cookie = '';

  async function request(method, url, body, headers = {}) {
    const sent = { ...headers };
    if (cookie) sent.Cookie = cookie;
    if (method !== 'GET') sent['Content-Type'] ??= 'application/json';
    const res = await fetch(base + url, {
      method,
      headers: sent,
      body: method === 'GET' ? undefined : JSON.stringify(body ?? {}),
    });
    const setCookie = res.headers.get('set-cookie');
    if (setCookie) cookie = setCookie.split(';')[0];
    const text = await res.text();
    const json = text && res.headers.get('content-type')
      ?.startsWith('application/json') ? JSON.parse(text) : null;
    return {
      status: res.status, headers: res.headers, body: json, text,
    };
  }

  const c = {
    get: (url, headers) => request('GET', url, undefined, headers),
    post: (url, body, headers) => request('POST', url, body, headers),
    patch: (url, body) => request('PATCH', url, body),
    put: (url, body) => request('PUT', url, body),
    async signIn(email, password = 'canteen-demo') {
      const res = await c.post('/api/auth/login', { email, password });
      if (res.status !== 200) {
        throw new Error(`sign-in as ${email} gave ${res.status}`);
      }
      return res.body.user;
    },
  };
  return c;
}

module.exports = { startApp, testClock };
munotes.in346

Testing the Project: the Levels, the Test Plan and the Tools

  • startApp() rebuilds the test database from the schema and the seed data, then builds the application with that database, a clock that stands still at 10:30 on 29 September 2026, and a quiet log. Every test file therefore starts from exactly the same data, and the tests can be run in any order.
  • The clock can be moved: app.clock.set(...) lets a test see what happens after a cut-off, or eight hours later, without waiting (Chapter 43).
  • client() keeps the set-cookie it receives and sends it back, so a test signs in once and then acts as that person. signIn(email) is the shorthand every test uses.
  • app.base is the address the server is listening on, chosen by the operating system with listen(0), so tests never collide over a port.

The tests never touch the real database. TEST_DB_NAME is a separate one, created in Chapter 44, and startApp empties and rebuilds it every time.

The test plan

A test plan is written before the testing starts, and says six things. MU prints no format, so this is the book's:

SectionWhat it saysThe worked project's
Scopewhat will be tested, and what will notevery functional and non-functional requirement; not the college network or MySQL itself
Approachthe levels and how each is derivedunit and integration written with the code; system from the SRS; acceptance with the owner and the counter
Environmentwhere each level runsunit and integration on the members' laptops and in the lab image; system and acceptance on the lab desktop
Whowho writes and runs each levelRohan owns the tests (WBS 5.1 to 5.4); Aditi owns system and acceptance (5.3)
Schedulewhenunit and integration with the code, to 5 Oct; integration and system 6 to 12 Oct; load and security 13 to 15 Oct
Pass criteriawhen testing is finishedevery test passes; every requirement has at least one test; no open defect of high severity

The last row is the one students leave out, and it is the one that makes a plan a plan. "Every requirement has at least one test" is checkable: it is the last column of the traceability matrix (Chapter 55).

What the suite is

$ cd ~/canteen-preorder
$ npm test 2>&1 | tail -3
  pass  refuses 29-09-2026

117 tests: 117 passed, 0 failed
$ for d in unit blackbox integration; do \
>   printf '%-12s ' "$d"; \
>   node --test --test-concurrency=1 \
>     --test-reporter=./test/reporter.js "test/$d/*.test.js" \
>     2>&1 | tail -1; done
unit         43 tests: 43 passed, 0 failed
blackbox     29 tests: 29 passed, 0 failed
integration  45 tests: 45 passed, 0 failed
$ ls test/unit test/blackbox test/integration
test/blackbox:
order-acceptance.test.js  order-input.test.js

test/integration:
auth.test.js	     counter.test.js  orders.test.js
concurrency.test.js  menu.test.js     security.test.js

test/unit:
attempts.test.js  passwords.test.js  rules.test.js  validate.test.js
munotes.in347

Testing the Project: the Levels, the Test Plan and the Tools

A hundred and seventeen tests in three folders: 43 unit tests of the pure functions and the small modules, 29 black-box tests derived from the requirements, and 45 integration tests that run against the real database. The unit and black-box tests need no database at all, which is why they finish in a moment. Chapters 51 to 53 take them one folder at a time, Chapter 54 adds system and acceptance testing, and Chapter 55 writes the test cases and the report.

What testing does not prove

Testing shows the presence of faults, not their absence: a suite that passes says only that these tests did not find anything. That is worth saying plainly in a report, and it is why the levels matter. The worked project's suite passed every day of the second increment, and system testing still found a message that read "Only 2 Chicken Biryani left. 3 2" (Chapters 49 and 56), because no test had asked what a person would see.

Do this for your project

  1. Plan the four levels, and say who writes and runs each.
  2. Use the test runner your language already has before adding a library.
  3. Write unit tests with the code, not after it.
  4. Give the integration tests their own database, rebuilt for every file.
  5. Make the clock something a test can set.
  6. Run everything with one command, and have a faster command for the unit tests alone.
  7. Write the pass criteria down, and make "every requirement has a test" one of them.

Mistakes that cost marks

Testing in the last week, when a failure can only be hidden.

Tests against the real database, which empty it or, worse, leave rubbish in it before the demonstration.

Tests that pass only in the order they were written, because each leaves data for the next.

A test that waits for real time to pass, so the suite takes minutes and nobody runs it.

"Tested by using the application", with nothing anyone else can run.

A plan with no pass criteria, so testing stops when the deadline arrives.

Quick revision

  • Levels: unit (one piece), integration (pieces together, real database), system (the whole against the SRS), acceptance (the users).
  • Black box from the specification; white box from the code. A project needs both.
  • Tools here: node:test and node:assert/strict, no library; a plain-words reporter.
  • One command (npm test), the pattern named, one file at a time, a separate test database rebuilt each time, a clock a test can set.
  • The plan: scope, approach, environment, who, schedule, pass criteria.
  • Testing shows the presence of faults, not their absence.
munotes.in348

Testing the Project: the Levels, the Test Plan and the Tools

Questions you must be able to answer

1. Name the four levels of testing and what each answers. Unit: does this one piece work on its own? Integration: do the pieces work together, with the real database? System: does the whole system do what the specification says? Acceptance: will the people who asked for it accept and use it?

2. What is the difference between black-box and white-box testing? Black-box tests are derived from the specification without looking at the code, so they test what the system should do. White-box tests are derived from the code's own structure, to reach paths the specification never mentions. A good suite has both.

3. Why do the integration tests use a separate database, rebuilt for every file? So that they never touch real data, and so that every file starts from exactly the same rows. Tests that depend on what an earlier test left behind pass in one order and fail in another.

4. Why does the test command name a pattern instead of letting the runner find the tests? Because a runner that walks the whole project will run anything that looks like a test, including scripts that are not tests: here the load-testing script, which reported a failure for want of a running server.

5. Why is the clock passed into the application rather than read? So that a test can set it: what happens a minute before a cut-off, a minute after, or eight hours later, can then be tested in milliseconds instead of waited for.

6. What should the pass criteria of a test plan say? When testing is finished: that every test passes, that every requirement has at least one test that covers it, and that no defect above an agreed severity is still open.

munotes.in349

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Issue
Done!