munotes®

Integration Testing

Get access to whole semester resourcesSemester Pass

Chapter Fifty-Three

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

Pages 363 to 372 of 499

In one line

An integration test starts the real application with a real database and sends it real requests, so that it tests what a unit test cannot: that the route, the guard, the validation, the service, the store, the SQL and the schema agree with each other.

In the wording to use when asked: integration testing exercises components together rather than in isolation, to detect faults in their interfaces and interactions: incorrect assumptions between layers, mismatched data or column names, transactions that do not span what they should, and configuration that differs from the unit tests' stubs. Tests at this level drive the system through its real external interface and assert on its real persisted state.

What integration testing catches

A unit test says a function works when it is called correctly. Most real faults are about whether it is called correctly, and by what:

A fault of this kindA unit testAn integration test
the route validates the body but not the id in the addresspassescatches
the store selects total_paise AS totalCost but the service reads totalPaisepassescatches
the transaction wraps the insert but not the stockpassescatches
the guard is on the wrong routepassescatches
the schema's column is 60 characters and the validator allows 80passescatches
the cookie is set without HttpOnlypassescatches

Everything in that list is an interface: two parts, each right on its own, that disagree. That is what this level is for, and it is why the tests use the real database rather than a pretend one. A stub of the database would agree with whatever the code assumed, which is the assumption being tested.

How one is written

Every file is the same shape (the helper is Chapter 50's):

describe('the counter, the kitchen and the day report', () => {
  let app;
  let ganesh;
  beforeEach(async () => {
    app = await startApp();
    ganesh = app.client();
    await ganesh.signIn('counter@college.example');
  });
  afterEach(() => app.stop());
  • beforeEach, not before: the database is rebuilt and the application started again for every test, so no test can depend on what another left behind. It costs a fraction of a second and buys tests that can be run in any order, or alone.
  • The client keeps its cookie, so signing in once makes every later request that person's. Two clients in one test are two people, which is how the tests about one student and another are written.
  • afterEach stops the server and closes the pool, so a suite of six files does not leave six servers listening.

The orders' tests

'use strict';

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

const VEG_THALI = 1;
const VEG_BIRYANI = 2;
const CHOLE_BHATURE = 5; // switched off in the seed data
const SAMOSA = 7;

describe('placing and cancelling orders', () => {
  let app;
  let priya;
  beforeEach(async () => {
    app = await startApp();
    priya = app.client();
    await priya.signIn('priya@college.example');
  });
  afterEach(() => app.stop());

  async function stockOf(id) {
    const { body } = await app.client().get('/api/menu');
    return body.items.find((item) => item.id === id).stockLeft;
  }

  async function ownerSetsStock(id, stockLeft) {
    const lata = app.client();
    await lata.signIn('owner@college.example');
    await lata.put(`/api/menu/${id}/stock`, { stockLeft });
  }

  it('places an order and takes it from the stock', async () => {
    const res = await priya.post('/api/orders', {
      slot: '12:40',
      items: [
        { menuItemId: VEG_THALI, quantity: 2 },
        { menuItemId: SAMOSA, quantity: 1 },
      ],
    });
    assert.equal(res.status, 201);
    const { order } = res.body;
    assert.equal(order.slot, '12:40');
    assert.equal(order.pickupDate, '2026-09-29');
    assert.equal(order.status, 'placed');
    assert.equal(order.totalPaise, 16000);
    assert.equal(order.createdAt, '2026-09-29 10:30:00');
    assert.equal(order.items.length, 2);
    assert.equal(await stockOf(VEG_THALI), 58);
    assert.equal(await stockOf(SAMOSA), 99);
  });

  it('refuses more than is left, and takes nothing', async () => {
    await ownerSetsStock(VEG_BIRYANI, 2);
    const res = await priya.post('/api/orders', {
      slot: '12:40', items: [{ menuItemId: VEG_BIRYANI, quantity: 3 }],
    });
    assert.equal(res.status, 409);
    assert.equal(res.body.error.message, 'Only 2 Veg Biryani left.');
    assert.equal(await stockOf(VEG_BIRYANI), 2);
  });

  it('keeps nothing of an order that fails half way', async () => {
    await ownerSetsStock(VEG_BIRYANI, 2);
    const res = await priya.post('/api/orders', {
      slot: '12:40',
      items: [
        { menuItemId: VEG_THALI, quantity: 1 },
        { menuItemId: VEG_BIRYANI, quantity: 3 },
      ],
    });
    assert.equal(res.status, 409);
    // The thali was taken first, then given back by the
    // rollback when the biryani failed.
    assert.equal(await stockOf(VEG_THALI), 60);
  });

  it('refuses an item that is switched off', async () => {
    const res = await priya.post('/api/orders', {
      slot: '12:40',
      items: [{ menuItemId: CHOLE_BHATURE, quantity: 1 }],
    });
    assert.equal(res.status, 409);
    assert.equal(res.body.error.code, 'item_unavailable');
  });

  it("shows a student their own orders and hides everyone else's",
    async () => {
      const placed = await priya.post('/api/orders', {
        slot: '12:50', items: [{ menuItemId: SAMOSA, quantity: 2 }],
      });
      const id = placed.body.order.id;
      const mine = await priya.get('/api/orders/mine');
      assert.deepEqual(mine.body.orders.map((o) => o.id), [id]);
      const kabir = app.client();
      await kabir.signIn('kabir@college.example');
      assert.equal((await kabir.get(`/api/orders/${id}`)).status, 404);
      assert.equal((await kabir.post(`/api/orders/${id}/cancel`))
        .status, 404);
    });

  it('cancels a placed order and puts the food back', async () => {
    const placed = await priya.post('/api/orders', {
      slot: '12:30', items: [{ menuItemId: SAMOSA, quantity: 3 }],
    });
    const id = placed.body.order.id;
    const res = await priya.post(`/api/orders/${id}/cancel`);
    assert.equal(res.body.order.status, 'cancelled');
    assert.equal(await stockOf(SAMOSA), 100);
  });

  it('will not cancel an order already being prepared', async () => {
    const placed = await priya.post('/api/orders', {
      slot: '12:30', items: [{ menuItemId: SAMOSA, quantity: 1 }],
    });
    const id = placed.body.order.id;
    const ganesh = app.client();
    await ganesh.signIn('counter@college.example');
    await ganesh.patch(`/api/orders/${id}/status`,
      { status: 'preparing' });
    const res = await priya.post(`/api/orders/${id}/cancel`);
    assert.equal(res.status, 409);
    assert.equal(res.body.error.message, 'This order is being '
      + 'prepared, so it cannot be cancelled now.');
  });
});
munotes.in363

Integration Testing

Each test asserts on what is in the database afterwards, not only on what the API answered:

munotes.in364

Integration Testing

  • placing an order gives 201 with the right total and date, and the stock falls by exactly the quantities ordered;
  • an order for more than is left is refused and the stock is untouched;
  • an order whose second item is short leaves the first item's stock exactly as it was, which is the transaction of Chapter 45 seen from outside;
  • cancelling puts the food back;
  • an order already being prepared cannot be cancelled, and the message says why.

That last assertion is on a sentence a student reads, and it is deliberate: the wording of a refusal is part of the behaviour the SRS asks for (FR-13).

The counter's tests

'use strict';

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

const VEG_THALI = 1;
const SAMOSA = 7;

describe('the counter, the kitchen and the day report', () => {
  let app;
  let ganesh;
  beforeEach(async () => {
    app = await startApp();
    ganesh = app.client();
    await ganesh.signIn('counter@college.example');
  });
  afterEach(() => app.stop());

  async function orderAs(email, slot, items) {
    const c = app.client();
    await c.signIn(email);
    const res = await c.post('/api/orders', { slot, items });
    assert.equal(res.status, 201);
    return res.body.order.id;
  }

  const move = (id, status) =>
    ganesh.patch(`/api/orders/${id}/status`, { status });

  it('moves an order through every status', async () => {
    const id = await orderAs('priya@college.example', '12:40',
      [{ menuItemId: SAMOSA, quantity: 1 }]);
    for (const status of ['preparing', 'ready', 'collected']) {
      const res = await move(id, status);
      assert.equal(res.status, 200);
      assert.equal(res.body.order.status, status);
    }
  });

  it('refuses a skipped step', async () => {
    const id = await orderAs('priya@college.example', '12:40',
      [{ menuItemId: SAMOSA, quantity: 1 }]);
    const res = await move(id, 'ready');
    assert.equal(res.status, 409);
    assert.deepEqual(res.body.error.details,
      { from: 'placed', to: 'ready' });
  });

  it('refuses a status change from a student', async () => {
    const id = await orderAs('priya@college.example', '12:40',
      [{ menuItemId: SAMOSA, quantity: 1 }]);
    const priya = app.client();
    await priya.signIn('priya@college.example');
    const res = await priya.patch(`/api/orders/${id}/status`,
      { status: 'preparing' });
    assert.equal(res.status, 403);
  });

  it('lists one slot of the day for the counter', async () => {
    await orderAs('priya@college.example', '12:40',
      [{ menuItemId: SAMOSA, quantity: 1 }]);
    await orderAs('kabir@college.example', '12:50',
      [{ menuItemId: SAMOSA, quantity: 1 }]);
    const res = await ganesh.get('/api/orders?slot=12:40');
    assert.equal(res.body.date, '2026-09-29');
    assert.deepEqual(res.body.orders.map((o) => o.studentName),
      ['Priya Menon']);
  });

  it('adds up what the kitchen still has to make', async () => {
    const a = await orderAs('priya@college.example', '12:40', [
      { menuItemId: VEG_THALI, quantity: 2 },
      { menuItemId: SAMOSA, quantity: 1 },
    ]);
    await orderAs('kabir@college.example', '12:40',
      [{ menuItemId: VEG_THALI, quantity: 1 }]);
    const done = await orderAs('ananya@college.example', '12:40',
      [{ menuItemId: VEG_THALI, quantity: 4 }]);
    await move(a, 'preparing');
    for (const s of ['preparing', 'ready', 'collected']) {
      await move(done, s);
    }
    const res = await ganesh.get('/api/kitchen?slot=12:40');
    assert.deepEqual(res.body.items, [
      { name: 'Samosa', quantity: 1 },
      { name: 'Veg Thali', quantity: 3 },
    ]);
  });

  it('counts only collected orders as sales', async () => {
    const paid = await orderAs('priya@college.example', '12:40',
      [{ menuItemId: VEG_THALI, quantity: 2 }]);
    const gone = await orderAs('kabir@college.example', '12:40',
      [{ menuItemId: VEG_THALI, quantity: 1 }]);
    for (const s of ['preparing', 'ready', 'collected']) {
      await move(paid, s);
    }
    for (const s of ['preparing', 'ready', 'no_show']) {
      await move(gone, s);
    }
    const lata = app.client();
    await lata.signIn('owner@college.example');
    const { report } = (await lata.get('/api/reports/daily')).body;
    assert.deepEqual(report.byStatus, [
      { status: 'collected', orders: 1 },
      { status: 'no_show', orders: 1 },
    ]);
    assert.deepEqual(report.items,
      [{ name: 'Veg Thali', quantity: 2, revenuePaise: 14000 }]);
    assert.equal(report.revenuePaise, 14000);
  });

  it('keeps the day report to the owner', async () => {
    const res = await ganesh.get('/api/reports/daily');
    assert.equal(res.status, 403);
  });
});
munotes.in365

Integration Testing

The kitchen test is the one worth studying. It places three orders, moves one to being prepared and another all the way to collected, and then asks what the kitchen still has to make. The answer, 3 thalis and 1 samosa, is arithmetic across three tables and two statuses (FR-16), and there is no way to check it but to put real orders in a real database and ask.

The day report's test does the same for FR-17: only collected orders count as sales, so an order that was cancelled or not collected must not appear in the takings.

The sign-in tests

'use strict';

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

describe('accounts and sign-in', () => {
  let app;
  beforeEach(async () => {
    app = await startApp();
  });
  afterEach(() => app.stop());

  it('registers a student with a college address', async () => {
    const res = await app.client().post('/api/auth/register', {
      name: 'Meera Joshi', email: 'meera@college.example',
      password: 'a-long-password',
    });
    assert.equal(res.status, 201);
    assert.equal(res.body.user.role, 'student');
    assert.equal(res.body.user.passwordHash, undefined);
  });

  it('refuses an address at any other domain', async () => {
    const res = await app.client().post('/api/auth/register', {
      name: 'Meera Joshi', email: 'meera@gmail.com',
      password: 'a-long-password',
    });
    assert.equal(res.status, 400);
    assert.equal(res.body.error.details.email,
      'Use your @college.example address.');
  });

  it('refuses a common password, naming the field', async () => {
    const res = await app.client().post('/api/auth/register', {
      name: 'Meera Joshi', email: 'meera@college.example',
      password: 'sunshine1',
    });
    assert.equal(res.status, 400);
    assert.equal(res.body.error.details.password,
      'This password is too common. '
      + 'Choose one that is hard to guess.');
  });

  it('refuses an email that already has an account', async () => {
    const res = await app.client().post('/api/auth/register', {
      name: 'Priya Menon', email: 'priya@college.example',
      password: 'a-long-password',
    });
    assert.equal(res.status, 409);
    assert.equal(res.body.error.code, 'email_taken');
  });

  it('signs in, knows who is signed in, and signs out', async () => {
    const priya = app.client();
    const res = await priya.post('/api/auth/login', {
      email: 'priya@college.example', password: 'canteen-demo',
    });
    assert.equal(res.status, 200);
    const cookie = res.headers.get('set-cookie');
    assert.match(cookie, /^sid=[\w-]{43};/);
    assert.match(cookie, /HttpOnly/);
    assert.match(cookie, /SameSite=Lax/);
    assert.equal((await priya.get('/api/auth/me')).body.user.name,
      'Priya Menon');
    assert.equal((await priya.post('/api/auth/logout')).status, 204);
    assert.equal((await priya.get('/api/auth/me')).status, 401);
  });

  it('forgets a sign-in once its eight hours are up', async () => {
    const priya = app.client();
    await priya.signIn('priya@college.example');
    app.clock.set('2026-09-29T18:29:00+05:30');
    assert.equal((await priya.get('/api/auth/me')).status, 200);
    app.clock.set('2026-09-29T18:30:00+05:30');
    assert.equal((await priya.get('/api/auth/me')).status, 401);
  });

  it('answers a wrong password and an unknown email alike',
    async () => {
      const c = app.client();
      const wrong = await c.post('/api/auth/login', {
        email: 'priya@college.example', password: 'not-her-password',
      });
      const unknown = await c.post('/api/auth/login', {
        email: 'nobody@college.example', password: 'not-her-password',
      });
      assert.equal(wrong.status, 401);
      assert.equal(unknown.status, 401);
      assert.deepEqual(wrong.body, unknown.body);
    });

  it('refuses the sixth try after five wrong passwords', async () => {
    const c = app.client();
    const guess = { email: 'priya@college.example', password: 'guess' };
    for (let i = 0; i < 5; i += 1) {
      const res = await c.post('/api/auth/login', guess);
      assert.equal(res.status, 401);
    }
    const res = await c.post('/api/auth/login', guess);
    assert.equal(res.status, 429);
    // The countdown runs from the FIRST failure, so the wait
    // is a little under the whole 15 minutes by the time the
    // five guesses have been made and hashed.
    const wait = Number(res.headers.get('retry-after'));
    assert.ok(wait > 870 && wait <= 900, `retry-after was ${wait}`);
  });

  it('lets the owner create a counter staff account', async () => {
    const lata = app.client();
    await lata.signIn('owner@college.example');
    const made = await lata.post('/api/users/staff', {
      name: 'Meena Rane', email: 'meena@college.example',
      password: 'counter-side-door-7',
    });
    assert.equal(made.status, 201);
    assert.equal(made.body.user.role, 'staff');
    // The new account can sign in and see the counter's list.
    const meena = app.client();
    await meena.signIn('meena@college.example', 'counter-side-door-7');
    assert.equal((await meena.get('/api/orders')).status, 200);
  });

  it('keeps staff accounts to the owner', async () => {
    const priya = app.client();
    await priya.signIn('priya@college.example');
    const res = await priya.post('/api/users/staff', {
      name: 'Meena Rane', email: 'meena@college.example',
      password: 'counter-side-door-7',
    });
    assert.equal(res.status, 403);
    assert.equal(res.body.error.code, 'forbidden');
  });

  it('refuses a change that is not sent as JSON', async () => {
    const res = await app.client().post('/api/auth/login', {},
      { 'Content-Type': 'text/plain' });
    assert.equal(res.status, 415);
  });

  it('refuses a change sent from another website', async () => {
    const res = await app.client().post('/api/auth/logout', {},
      { Origin: 'https://evil.example' });
    assert.equal(res.status, 403);
    assert.equal(res.body.error.code, 'wrong_origin');
  });
});
munotes.in366

Integration Testing

These are the tests of the non-functional requirements that can be tested from outside:

  • NFR-4, the cookie: HttpOnly, SameSite=Lax, and a token of the right length are asserted on the real Set-Cookie header;
  • the eight hours: the test moves the clock forward and finds the session gone;
  • NFR-6, the limit: five failures and the sixth is refused, with a Retry-After in the right range;
  • S16: a wrong password and an unknown email answer alike;
  • S5: a common password is refused, with the message on the password field;
  • S7: a change that is not JSON, and one from another site, are both refused;
  • FR-3: the owner creates a counter staff account, who can then sign in and see the counter's list, and a student trying the same is refused.
munotes.in367

Integration Testing

The security tests

'use strict';

// Controls from the security design (Chapter 31) that no other
// test file proves. Each test is named after its control.

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

describe('the security design', () => {
  let app;
  afterEach(() => app.stop());

  it('S6 keeps only a hash of the session token', async () => {
    app = await startApp();
    const res = await app.client().post('/api/auth/login', {
      email: 'priya@college.example', password: 'canteen-demo',
    });
    const token = res.headers.get('set-cookie')
      .match(/^sid=([^;]+)/)[1];
    const [rows] = await app.pool.query('SELECT id FROM sessions');
    const ids = rows.map((row) => row.id);
    assert.equal(ids.includes(token), false);
    assert.ok(ids.includes(
      crypto.createHash('sha256').update(token).digest('hex')));
  });

  it('S8 treats SQL typed into a field as text', async () => {
    app = await startApp();
    const res = await app.client().post('/api/auth/login', {
      email: "' OR '1'='1", password: "' OR '1'='1",
    });
    assert.equal(res.status, 401);
    const lata = app.client();
    await lata.signIn('owner@college.example');
    const name = "Tea'); DROP TABLE orders; --";
    const added = await lata.post('/api/menu', {
      name, category: 'drinks', pricePaise: 1500, isVeg: true,
    });
    assert.equal(added.status, 201);
    assert.equal(added.body.item.name, name);
    const ganesh = app.client();
    await ganesh.signIn('counter@college.example');
    assert.equal((await ganesh.get('/api/orders')).status, 200);
  });

  it('S10 refuses a request body over 10 kilobytes', async () => {
    app = await startApp();
    const priya = app.client();
    await priya.signIn('priya@college.example');
    const res = await priya.post('/api/orders', {
      slot: '12:40', items: [], note: 'x'.repeat(11000),
    });
    assert.equal(res.status, 413);
    assert.equal(res.body.error.code, 'too_large');
  });

  it('S11 answers a broken body without the details', async () => {
    app = await startApp();
    const res = await fetch(`${app.base}/api/auth/login`, {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: '{"email": ',
    });
    const text = await res.text();
    assert.equal(res.status, 400);
    assert.equal(JSON.parse(text).error.code, 'bad_json');
    assert.doesNotMatch(text, /SyntaxError|at JSON|stack/);
  });

  it('S12 marks the cookie Secure and sends HSTS under HTTPS',
    async () => {
      app = await startApp({ COOKIE_SECURE: 'true' });
      const res = await app.client().post('/api/auth/login', {
        email: 'priya@college.example', password: 'canteen-demo',
      });
      assert.match(res.headers.get('set-cookie'), /; Secure/);
      assert.equal(res.headers.get('strict-transport-security'),
        'max-age=15552000');
    });
});

Each is named after the control it proves (Chapter 31), so that the security report of Chapter 65 can be written by running the suite rather than by remembering. The SQL injection test is the one to look at: it stores a menu item named Tea'); DROP TABLE orders; -- and then checks that the orders table still answers, which is the demonstration of Chapter 44 turned into something that runs on every commit.

The menu's tests are the best file to read first, and its own name says why: "the menu, and the application as a whole". Twelve tests, of which the last five are not about the menu at all but about the application around it.

munotes.in368

Integration Testing

'use strict';

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

describe('the menu, and the application as a whole', () => {
  let app;
  let lata;
  beforeEach(async () => {
    app = await startApp();
    lata = app.client();
    await lata.signIn('owner@college.example');
  });
  afterEach(() => app.stop());

  it('shows the whole menu to anyone, meals first', async () => {
    const res = await app.client().get('/api/menu');
    assert.equal(res.status, 200);
    assert.equal(res.body.items.length, 14);
    assert.deepEqual(res.body.items[0], {
      id: 3, name: 'Chicken Biryani', category: 'meals',
      pricePaise: 11000, isVeg: false, isAvailable: true,
      stockLeft: 30,
    });
  });

  it('lets the owner add an item', async () => {
    const res = await lata.post('/api/menu', {
      name: 'Misal Pav', category: 'meals', pricePaise: 6000,
      isVeg: true,
    });
    assert.equal(res.status, 201);
    assert.equal(res.body.item.stockLeft, 0);
  });

  it('refuses a second item with the same name', async () => {
    const res = await lata.post('/api/menu', {
      name: 'Samosa', category: 'snacks', pricePaise: 2000,
      isVeg: true,
    });
    assert.equal(res.status, 409);
    assert.equal(res.body.error.code, 'name_taken');
  });

  it('lets the owner change a price and set the stock', async () => {
    await lata.patch('/api/menu/7', { pricePaise: 2500 });
    const res = await lata.put('/api/menu/7/stock', { stockLeft: 80 });
    assert.equal(res.body.item.pricePaise, 2500);
    assert.equal(res.body.item.stockLeft, 80);
  });

  it('keeps menu changes to the owner', async () => {
    const priya = app.client();
    await priya.signIn('priya@college.example');
    const change = { pricePaise: 100 };
    const asStudent = await priya.patch('/api/menu/7', change);
    const asStranger = await app.client().patch('/api/menu/7', change);
    assert.equal(asStudent.status, 403);
    assert.equal(asStranger.status, 401);
  });

  it('answers 404 for an item that does not exist', async () => {
    const res = await lata.patch('/api/menu/99', { pricePaise: 2500 });
    assert.equal(res.status, 404);
    assert.equal(res.body.error.message, 'Menu item not found.');
  });

  it('offers the four pickup slots with their cut-offs',
    async () => {
      const res = await app.client().get('/api/slots');
      assert.equal(res.status, 200);
      assert.deepEqual(res.body.slots, [
        { time: '12:30', cutoff: '12:15', open: true },
        { time: '12:40', cutoff: '12:25', open: true },
        { time: '12:50', cutoff: '12:35', open: true },
        { time: '13:00', cutoff: '12:45', open: true },
      ]);
    });

  it('closes the slots whose cut-off has passed', async () => {
    app.clock.set('2026-09-29T12:26:00+05:30');
    const { slots } = (await app.client().get('/api/slots')).body;
    assert.deepEqual(slots.map((s) => s.open),
      [false, false, true, true]);
  });

  it('reports itself up, with its database', async () => {
    const res = await app.client().get('/api/health');
    assert.deepEqual(res.body, { status: 'ok', database: 'ok' });
  });

  it('answers an unknown API address with JSON', async () => {
    const res = await app.client().get('/api/nothing-here');
    assert.equal(res.status, 404);
    assert.equal(res.body.error.code, 'not_found');
  });

  it('answers an unknown page with the 404 page', async () => {
    const res = await app.client().get('/nothing-here.html');
    assert.equal(res.status, 404);
    assert.match(res.text, /<title>Page not found/);
  });

  it('sends the security headers and hides the framework',
    async () => {
      const { headers } = await app.client().get('/api/menu');
      assert.match(headers.get('content-security-policy'),
        /default-src 'self'/);
      assert.equal(headers.get('x-content-type-options'), 'nosniff');
      assert.equal(headers.get('x-frame-options'), 'DENY');
      assert.equal(headers.get('x-powered-by'), null);
    });
});
munotes.in369

Integration Testing

Three habits in it are worth taking. The public route is tested without signing in at all, with a client that has no session, because that is how a student meets the menu before they have an account. Each refusal is checked as a pair: a student asking for the owner's change gets 403 and a stranger gets 401, which are different failures and are easy to confuse in the code. And the file tests the whole application while it is here: the health route, an unknown API address answering as JSON, an unknown page answering with the 404 page, and the security headers of Chapter 31, including that X-Powered-By is gone. Those five belong nowhere else, and putting them in one file is better than four half-used ones.

What an integration test catches that a unit test cannot

One way to see the point is to break something no unit test touches: the name of a column in the store.

$ cd ~/canteen-preorder
$ sed -i 's/o.total_paise AS totalPaise/o.total_paise AS totalCost/' \
>   src/store/orders.js
$ npm run test:unit 2>&1 | tail -1
43 tests: 43 passed, 0 failed
$ node --test --test-concurrency=1 \
>   --test-reporter=./test/reporter.js test/integration/orders.test.js \
>   2>&1 | grep -E 'FAIL|tests:' | head -4
  FAIL  places an order and takes it from the stock
7 tests: 6 passed, 1 failed
$ sed -i 's/o.total_paise AS totalCost/o.total_paise AS totalPaise/' \
>   src/store/orders.js
$ npm test 2>&1 | tail -1
117 tests: 117 passed, 0 failed

All forty-three unit tests still pass, because no unit test knows about the store. The integration test that reads an order back fails at once, and its name says where to look. One word changed in a SQL SELECT, a fault no amount of reading the services would show, caught in a second by the only level that asks the database.

Running them

$ cd ~/canteen-preorder
$ node --test --test-concurrency=1 \
>   --test-reporter=./test/reporter.js "test/integration/*.test.js" \
>   2>&1 | tail -3
  pass  S12 marks the cookie Secure and sends HSTS under HTTPS

45 tests: 45 passed, 0 failed

Forty-five tests, each against a freshly built database. They are slower than the unit tests, seconds rather than milliseconds, which is the price of testing the real thing, and the reason a developer runs npm run test:unit while writing and the whole suite before committing.

Do this for your project

  1. Test through your real interface, with your real database, in a database of its own.
  2. Rebuild that database before every test, and start the application from the same factory the server uses.
  3. Assert on the state afterwards, not only on the answer.
  4. Use one client per person, so that the tests read as people doing things.
  5. Cover every route, every guard and every refusal the API specification names.
  6. Name the tests of security controls after the controls, so the report can be produced by running them.
  7. Break something across a boundary once, and watch this level catch what the unit tests cannot.
munotes.in370

Integration Testing

Mistakes that cost marks

Mocking the database, which tests the mock's agreement with the code's assumptions.

One database for the tests and the application, emptied in the middle of a demonstration.

Tests that must run in order, because each leaves data for the next.

Servers left listening after the tests, until the port is taken and the suite fails for no reason.

Asserting only the status code, so a route that answers 200 with the wrong data passes.

No test for the refusals, which are most of what a real system does.

Quick revision

  • Integration tests catch interface faults: two parts, each right alone, that disagree.
  • The real database, in a database of its own, rebuilt before every test (beforeEach).
  • Same application factory as the server; one client per person, keeping its cookie.
  • Assert the answer and the stored state.
  • They test the NFRs visible from outside: cookie flags, session expiry, the sign-in limit, the security controls.
  • Slower than unit tests: run the unit tests while writing, all of them before committing.

Questions you must be able to answer

1. What does integration testing test that unit testing cannot? Whether the parts agree with each other: that routes validate what they receive, that the names the store selects are the names the service reads, that transactions cover what they should, that the guards are on the right routes, and that the schema's limits match the validator's.

2. Why use the real database rather than a stub? Because the thing being tested is the agreement between the code and the database: column names, types, constraints and transactions. A stub agrees with whatever the code assumed, so it can only confirm the assumption, never test it.

3. Why is the test database rebuilt before every test rather than every file? So that no test can depend on data another test left, and any test can be run alone or in any order. The cost is a fraction of a second; the benefit is that a failure means what it says.

4. What should an integration test assert? Both the answer and the state afterwards: the status and body the API returned, and what is now in the database, such as the stock having fallen by exactly the quantity ordered, or not having moved at all when the order was refused.

munotes.in371

Integration Testing

5. How do these tests cover non-functional requirements? By asserting what can be seen from outside: the cookie's flags and lifetime, a session that has expired, the refusal after five failed sign-ins with its Retry-After, identical answers for a wrong password and an unknown email, and each security control named in its test.

6. Give an example of a fault that only an integration test finds. A column renamed in a SQL SELECT, such as selecting total_paise AS totalCost where the service reads totalPaise. Every unit test still passes, because none of them touches the store, and every integration test that reads an order fails at once.

munotes.in372

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!