We use cookies to enhance your experience on the site
CodeWorlds

Performance Testing - endurance trials

The legion has already checked whether the machines work. A different question remains: how much will they withstand? A catapult that fires one shot is useless in a siege lasting a week. Performance tests measure exactly that - not whether the code works, but for how long and under what pressure.

Such trials, however, are unusually sensitive to mess. A test that leaves a thousand records in the database will distort the next one's result. So before we measure the machines' endurance, we will put the testing ground in order - and that order will serve you in every test, not only performance ones.

Where a test lives

Let's start with the file name, because a convention applies here that the whole NestJS ecosystem follows:

1legions.service.spec.ts

You read it left to right: what we are testing (

legions
), what kind of element it is (
service
), that this is a test file (
spec
), and finally the extension (
ts
). E2E tests get
.e2e-spec.ts
instead.

This convention is not decoration - Jest finds files by that pattern by default. A file named

testLegions.ts
will simply never run, and you will be left wondering why the CI watch shines green. We keep the file next to the code it tests, which makes it obvious at a glance what has tests and what does not.

The AAA pattern - three beats of one trial

The

describe
block grouping the tests of one element is called a suite. Inside it every test follows the same rhythm, known as AAA:

1it('handles 100 requests in under 2 seconds', async () => {
2  // Arrange - prepare data and doubles
3  const legions = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Legio ${i}` }));
4  jest.spyOn(repo, 'find').mockResolvedValue(legions);
5
6  // Act - call the method under test
7  const start = Date.now();
8  await Promise.all(Array.from({ length: 100 }, () => service.findAll()));
9  const duration = Date.now() - start;
10
11  // Assert - check the result
12  expect(duration).toBeLessThan(2000);
13});

The three beats match the three words. Arrange sets the scene: data, doubles, initial state. Act is a single call - the thing we are examining. Assert checks the result with one or more assertions.

The value of this split is practical: when a test fails, you know at once which beat to search. And when you find more than one call in the Act section, that is a sign the test examines two things at once and is worth splitting.

To the three beats a fourth is added, easily forgotten: Cleanup - tidying up after the test, known in the jargon as teardown. We usually do not write it in the test body but in a separate hook, and it is precisely what decides the honesty of performance trials.

Hooks - when to prepare, when to clean up

Jest offers four hooks, and choosing between them comes down to one question: should this happen once, or before every test?

1describe('LegionsService - performance', () => {
2  let service: LegionsService;
3  let module: TestingModule;
4
5  beforeAll(async () => {
6    module = await Test.createTestingModule({
7      providers: [LegionsService, { provide: getRepositoryToken(Legion), useValue: repo }],
8    }).compile();
9
10    service = module.get(LegionsService);
11  });
12
13  afterEach(() => {
14    jest.clearAllMocks();
15  });
16
17  afterAll(async () => {
18    await module.close();
19  });
20});

beforeAll
runs once before all the tests in the block - here we build the testing module, because it is an expensive operation and there is no reason to repeat it a hundred times.
beforeEach
would run before every test; that is where things which must be fresh belong, such as clearing a table.

On the other side,

afterEach
cleans up after each test - like resetting the doubles here - and
afterAll
closes what
beforeAll
opened: connections, the module, files.

The rule is symmetrical and worth remembering: what

beforeAll
opened,
afterAll
closes; what
beforeEach
prepared,
afterEach
clears
. An unclosed connection will make Jest hang after the last test, printing that something is still running.

Edge cases - where the machines crack

A test at typical load says little. The interesting part happens at the extremes, and for endurance trials there are three: zero, one and very many.

1describe('findAll under load', () => {
2  it('returns an empty array when there are no legions', async () => {
3    jest.spyOn(repo, 'find').mockResolvedValue([]);
4
5    await expect(service.findAll()).resolves.toEqual([]);
6  });
7
8  it('stays under 5 seconds with 10,000 legions', async () => {
9    jest.spyOn(repo, 'find').mockResolvedValue(makeLegions(10_000));
10
11    const start = Date.now();
12    await service.findAll();
13
14    expect(Date.now() - start).toBeLessThan(5000);
15  });
16});

The first test guards emptiness - where code likes to throw on

legions[0]
. The second is a spike: a sudden, many times larger load, checking whether the method scales sensibly or starts choking on every record individually.

One note about time thresholds, @name: set them with margin. A CI machine is often slower than yours, and a test that fails randomly once in ten runs will be switched off by the first person it blocks from deploying - and rightly so. A loose but trustworthy threshold beats a sharp and ignored one.

Summary

The testing ground is in order and the machines are measured:

  • performance tests ask not whether the code works, but for how long and under what pressure,
  • the file name is the convention Jest uses to find tests:
    name.service.spec.ts
    , and
    .e2e-spec.ts
    for E2E,
  • the AAA pattern:
    Arrange
    prepares data and doubles,
    Act
    calls the method under test,
    Assert
    checks the result, and
    Cleanup
    tidies up,
  • more than one call in the
    Act
    section means the test examines two things at once,
  • a suite is a
    describe
    block; preparation is called setup, cleaning up teardown,
  • beforeAll
    and
    afterAll
    handle what is expensive and one-off;
    beforeEach
    and
    afterEach
    what must be fresh for every test,
  • what
    beforeAll
    opened,
    afterAll
    closes
    - an unclosed connection hangs Jest after the last test,
  • the edge cases for endurance are zero, one and very many; the last of these is a spike,
  • set time thresholds with margin - a randomly failing test gets switched off and stops protecting anything.

In the next lesson these trials will stop depending on your memory - we will let them into an automated watch in CI. For now remember: the AAA pattern is three beats of one trial, and the hooks make sure each begins on a clean ground.

Go to CodeWorlds