munotes®

Black-Box Testing: Equivalence Partitions, Boundary Values and Decision Tables

Get access to whole semester resourcesSemester Pass

Chapter Fifty-Two

Syllabus topic Module 2, "Integration & System Testing: Black-box testing".

Pages 357 to 362 of 499

In one line

Black-box testing derives its tests from what the system is supposed to do, never from how it is written: the values are divided into groups that should behave alike, one from each group is tried, the values either side of every edge are tried because that is where mistakes live, and where several conditions combine, a table lists the combinations so that none is forgotten.

In the wording to use when asked: black-box, or specification-based, testing derives test cases from the requirements without reference to the internal structure of the code. Equivalence partitioning divides the input domain into classes whose members should be treated alike, so that one value from each suffices; boundary value analysis adds the values at and immediately either side of each partition's edges, where off-by-one faults occur; and decision tables enumerate the combinations of conditions and the action expected of each.

Why derive tests from the specification

A test written by reading the code tends to agree with the code, including where the code is wrong. The mistake it cannot find is the one the programmer made twice: once in the code, once in the test.

So the tests in this chapter are written from the SRS alone (Chapter 34):

FR-7. Place an order. The system shall let a signed-in student order one or more items that can be ordered now, from 1 to 5 of each item and no more than 10 items in all, for one pickup slot.

Everything below comes out of that sentence and its neighbours, and would be the same whatever language the system were written in.

Equivalence partitioning

A partition is a set of values that should all be treated the same way. If 3 of an item is accepted, so are 2 and 4: trying all three tests the same thing three times. Divide the input into partitions, and test one value from each.

For the quantity of one item, FR-7 gives:

PartitionExampleExpected
below the range0refused
the valid range, 1 to 53accepted
above the range6refused
not a whole number2.5refused
not a number at all"2", null, truerefused

The last two partitions are the ones students forget, and they are where a careless system says yes: "2" is accepted by anything that converts before it checks (Chapter 47).

Boundary value analysis

Most faults are not in the middle of a partition but at its edge: < written for <=, a loop that stops one short. Boundary value analysis tests the edges and the values immediately either side.

For 1 to 5, the interesting values are 0, 1, 2, 4, 5, 6: just below, the lower edge, just inside, just inside the top, the upper edge, just above. Six tests, which is exactly what the file has:

munotes.in357

Black-Box Testing: Equivalence Partitions, Boundary Values and Decision Tables

'use strict';

// Black-box tests of the order form's rules, derived from
// the requirement alone (SRS FR-7 and FR-8), not from the
// code: 1 to 5 of each item, at most 10 items in all, one of
// the four pickup slots.

const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const validate = require('../../src/validate');

const order = (items, slot = '12:40') => ({ slot, items });
const one = (quantity) => [{ menuItemId: 1, quantity }];

function accepted(body) {
  try {
    validate.order(body);
    return true;
  } catch (err) {
    if (err.status !== 400) throw err;
    return false;
  }
}

describe('quantity of one item: boundary values', () => {
  const cases = [
    [0, false], // just below the valid partition
    [1, true], // its lower edge
    [2, true], // just inside
    [4, true], // just inside the top
    [5, true], // its upper edge
    [6, false], // just above
  ];
  for (const [quantity, ok] of cases) {
    it(`${quantity} is ${ok ? 'accepted' : 'refused'}`, () => {
      assert.equal(accepted(order(one(quantity))), ok);
    });
  }
});

describe('quantity: the invalid partitions that are not numbers',
  () => {
    for (const q of [2.5, '2', null, undefined, true]) {
      it(`${JSON.stringify(q)} is refused`, () => {
        assert.equal(accepted(order(one(q))), false);
      });
    }
  });

describe('items in one order: boundary values', () => {
  const lines = (...qs) => qs.map((quantity, i) => ({
    menuItemId: i + 1, quantity,
  }));
  it('10 items in all is accepted', () => {
    assert.equal(accepted(order(lines(5, 5))), true);
  });
  it('11 items in all is refused', () => {
    assert.equal(accepted(order(lines(5, 5, 1))), false);
  });
  it('no items at all is refused', () => {
    assert.equal(accepted(order([])), false);
  });
  it('the same item twice is refused', () => {
    assert.equal(accepted(order([
      { menuItemId: 3, quantity: 1 }, { menuItemId: 3, quantity: 1 },
    ])), false);
  });
});

describe('pickup slot: every valid value and the invalid ones',
  () => {
    for (const slot of ['12:30', '12:40', '12:50', '13:00']) {
      it(`${slot} is accepted`, () => {
        assert.equal(accepted(order(one(1), slot)), true);
      });
    }
    // Built by hand, not with order(): its default parameter
    // would turn a missing slot into '12:40' and test nothing.
    for (const slot of ['12:35', '13:10', '', undefined]) {
      it(`${JSON.stringify(slot)} is refused`, () => {
        assert.equal(accepted({ slot, items: one(1) }), false);
      });
    }
  });

Read what else is in it:

  • The order's total size, FR-7's "no more than 10 items in all": 10 accepted, 11 refused. Two tests at the edge, and none in the middle.
  • Every valid slot, not one. There are only four (FR-8), so each is tried; the invalid ones are a time that is not a slot, a time after the last one, an empty value and none at all.
  • The comment that explains a trap. The helper order() has a default slot, so a test for a missing slot must not use it, or it would test 12:40 and pass for the wrong reason. A test that can pass for the wrong reason is worse than no test.
  • The same item twice is refused: a case no partition suggests, and which comes from reading FR-7 carefully.
munotes.in358

Black-Box Testing: Equivalence Partitions, Boundary Values and Decision Tables

The test names are generated from the values, so the report reads as a table of what the system accepts:

$ cd ~/canteen-preorder
$ node --test --test-reporter=./test/reporter.js \
>   test/blackbox/order-input.test.js 2>&1 | head -18
test/blackbox/order-input.test.js
  pass  0 is refused
  pass  1 is accepted
  pass  2 is accepted
  pass  4 is accepted
  pass  5 is accepted
  pass  6 is refused
  pass  2.5 is refused
  pass  "2" is refused
  pass  null is refused
  pass  undefined is refused
  pass  true is refused
  pass  10 items in all is accepted
  pass  11 items in all is refused
  pass  no items at all is refused
  pass  the same item twice is refused
  pass  12:30 is accepted
  pass  12:40 is accepted

Decision tables

Some rules are not about one value but about a combination of conditions. Placing an order has four, and a fifth that depends on them:

ConditionFrom
Is someone signed in?FR-2
Are they a student?FR-7
Is the slot still open?FR-8
Do they already have an order for that slot?FR-10
Is there enough stock?FR-9

A decision table lists the combinations and what each should do. Written in full, five yes-or-no conditions would be 32 rules, most of them unreachable: if nobody is signed in, nothing else matters. So the table is written as rules that fire in order, each with the first condition that decides it:

RuleSigned inStudentSlot openNo order yetEnough stockExpected
R1yesyesyesyesyes201, the order
R2no----401 not_signed_in
R3yesno---403 forbidden
R4yesyesno--409 slot_closed
R5yesyesyesno-409 slot_taken
R6yesyesyesyesno409 sold_out

A dash means "it does not matter", and it is the heart of the technique: naming the combinations that cannot change the answer is what turns 32 rules into 6. Each row becomes one test, named after its rule:

'use strict';

// Black-box test of FR-7 to FR-9 as a decision table: which
// combination of conditions accepts an order, and which
// refusal each other combination gets. One test per rule.

const { describe, it, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const { startApp } = require('../helpers');

const THALI = {
  slot: '12:40', items: [{ menuItemId: 1, quantity: 1 }],
};

describe('decision table: is an order accepted?', () => {
  let app;
  beforeEach(async () => {
    app = await startApp();
  });
  afterEach(() => app.stop());

  async function student() {
    const priya = app.client();
    await priya.signIn('priya@college.example');
    return priya;
  }

  it('R1 every condition met: accepted, 201', async () => {
    const res = await (await student()).post('/api/orders', THALI);
    assert.equal(res.status, 201);
  });

  it('R2 not signed in: 401', async () => {
    const res = await app.client().post('/api/orders', THALI);
    assert.equal(res.status, 401);
  });

  it('R3 signed in, but not as a student: 403', async () => {
    const ganesh = app.client();
    await ganesh.signIn('counter@college.example');
    const res = await ganesh.post('/api/orders', THALI);
    assert.equal(res.status, 403);
  });

  it('R4 slot closed: 409 slot_closed', async () => {
    const priya = await student();
    app.clock.set('2026-09-29T12:25:00+05:30');
    const res = await priya.post('/api/orders', THALI);
    assert.equal(res.status, 409);
    assert.equal(res.body.error.code, 'slot_closed');
  });

  it('R5 already has an order for the slot: 409 slot_taken',
    async () => {
      const priya = await student();
      await priya.post('/api/orders', THALI);
      const res = await priya.post('/api/orders', THALI);
      assert.equal(res.status, 409);
      assert.equal(res.body.error.code, 'slot_taken');
    });

  it('R6 not enough stock: 409 sold_out', async () => {
    const lata = app.client();
    await lata.signIn('owner@college.example');
    await lata.put('/api/menu/1/stock', { stockLeft: 0 });
    const res = await (await student()).post('/api/orders', THALI);
    assert.equal(res.status, 409);
    assert.equal(res.body.error.code, 'sold_out');
  });
});
munotes.in359

Black-Box Testing: Equivalence Partitions, Boundary Values and Decision Tables

$ cd ~/canteen-preorder
$ node --test --test-reporter=./test/reporter.js \
>   test/blackbox/order-acceptance.test.js 2>&1 | tail -9
test/blackbox/order-acceptance.test.js
  pass  R1 every condition met: accepted, 201
  pass  R2 not signed in: 401
  pass  R3 signed in, but not as a student: 403
  pass  R4 slot closed: 409 slot_closed
  pass  R5 already has an order for the slot: 409 slot_taken
  pass  R6 not enough stock: 409 sold_out

6 tests: 6 passed, 0 failed

Three things about these tests:

  • They go through the API, not through a function, because the conditions they combine live in different layers: the guard, the rules and the store. A black-box test may use whatever door the specification describes.
  • Each sets up only its own condition: R4 moves the clock past the cut-off, R5 places an order first, R6 sets the stock to nothing. The rest stays as the seed data leaves it.
  • The expected value is the status and the code, which is what the specification promises (Chapter 30), not a message that may be reworded.

Choosing what to test, in order

Put together, the three techniques give a way of deriving a whole suite from a requirement, and an order to do it in:

  1. Partition every input, and include the partitions that are not the right kind at all.
  2. Take the boundaries of every range: the edge, and the value either side.
  3. Table the combinations where more than one condition decides the answer, and collapse the rules that cannot matter.
  4. Add the cases the specification names in words: "the same item twice", "one active order per slot".
  5. Add every case a bug has ever caused (Chapter 56).
munotes.in360

Black-Box Testing: Equivalence Partitions, Boundary Values and Decision Tables

The worked project's 29 black-box tests come from exactly that, and they are the tests that would still be right if the whole application were rewritten in another language.

Do this for your project

  1. Write your black-box tests from your SRS, with the code out of sight.
  2. Partition every input; include the wrong-type and missing partitions.
  3. Test the edge of every range and the value either side of it.
  4. Build a decision table where conditions combine, and collapse the rules that cannot matter.
  5. Name each test after the value or the rule, so the report reads as a specification.
  6. Check that every test can only pass for the right reason; beware default values in your helpers.
  7. Assert the status and the code, not the wording of a message.

Mistakes that cost marks

Testing 1, 2 and 3 of a range that goes to 5, and never 5 or 6.

Only the happy path, with no test for the wrong type or a missing value.

Tests written from the code, which agree with it exactly where it is wrong.

A combination table with 32 rows, most of them impossible, instead of six that matter.

A test that passes because a helper filled in a value, not because the system behaved.

Asserting the English of a message, so rewording it breaks the suite.

Quick revision

  • Black box: tests from the specification, not the code.
  • Equivalence partition: one value from each group that should behave alike, including the wrong kinds.
  • Boundary values: the edge and either side; for 1 to 5, test 0, 1, 2, 4, 5, 6.
  • Decision table: the conditions, the rules, a dash for what cannot matter; 5 conditions collapse to 6 rules.
  • Name tests after their value or rule; assert status and code.
  • Beware a helper's default value letting a test pass for the wrong reason.

Questions you must be able to answer

1. What is black-box testing? Testing derived from what the system is specified to do, with no reference to how it is written, so the tests would be the same for any implementation and can catch a fault the programmer built into both the code and their own idea of it.

2. What is equivalence partitioning, and why does it save work? Dividing the possible inputs into groups whose members should all be treated the same way, and testing one value from each. Testing 2, 3 and 4 of an item tests one partition three times; testing 0, 3 and 6 tests three.

munotes.in361

Black-Box Testing: Equivalence Partitions, Boundary Values and Decision Tables

3. What is boundary value analysis, and which values does it choose for a range of 1 to 5? Testing the edges of each partition and the values immediately either side, because off-by-one faults live there: 0, 1, 2, 4, 5 and 6.

4. What is a decision table, and what does a dash in one mean? A table of the combinations of conditions that decide an outcome, with the expected result of each. A dash means the condition cannot change the result for that rule, which is what collapses an impractical number of combinations into a few that matter.

5. Why do the decision-table tests go through the API rather than call a function? Because the conditions they combine are enforced in different layers: whether someone is signed in, whether they are a student, whether the slot is open, and whether the stock is there. Only a request exercises all of them together.

6. Why does one test in the worked file avoid the helper that builds an order? Because the helper supplies a default slot, so a test for a missing slot would silently test the default and pass without testing anything. A test that can pass for the wrong reason gives false confidence.

munotes.in362

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!