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.
Let's start with the file name, because a convention applies here that the whole NestJS ecosystem follows:
1legions.service.spec.tsYou 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
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.
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
opened, beforeAll
closes; what afterAll
prepared, beforeEach
clears. An unclosed connection will make Jest hang after the last test, printing that something is still running.afterEach
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.
The testing ground is in order and the machines are measured:
name.service.spec.ts, and .e2e-spec.ts for E2E,Arrange prepares data and doubles, Act calls the method under test, Assert checks the result, and Cleanup tidies up,Act section means the test examines two things at once,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,beforeAll opened, afterAll closes - an unclosed connection hangs Jest after the last test,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.