We use cookies to enhance your experience on the site
CodeWorlds

Unit Testing - testing the fort's individual parts

The catapult will not fire, but the culprit is nowhere in sight. Maybe a rope snapped, maybe the release mechanism failed, maybe the counterweight was miscalculated. Checking the whole machine at once tells you only that something is wrong - not what.

The legion's mechanic proceeds differently: he takes the machine apart and examines each part separately. Does the rope hold the tension? Does the mechanism release when pulled? A unit test does exactly the same with code - it checks the smallest element in isolation from the rest of the system.

The three words a test is built from

Let's start with the simplest case - a class with no dependencies at all:

1describe('Bolt', () => {
2  it('calculates the projectile weight', () => {
3    const bolt = new Bolt('iron', 5);
4
5    expect(bolt.getWeight()).toBe(5);
6  });
7});

Three words and you have a test.

describe
groups the tests concerning one element - it is a chapter heading.
it
describes one behaviour, and its name reads as a sentence: "it calculates the projectile weight".
expect
states an expectation and compares it with reality, here through the
toBe
matcher.

Note the name inside

it
: it says what the element should do, not how it is built. When such a test fails in a CI report, you read "calculates the projectile weight" and know at once what broke - without opening the file.

The problem: a service is not created with new

We created the

Bolt
class with a plain
new
. But a NestJS service has injected dependencies - a repository, configuration, other services. Writing
new LegionsService()
will end in an error, because the constructor expects something.

You could pass those dependencies by hand, but then you bypass the whole injection mechanism and the test stops resembling reality. So NestJS offers a testing module: a miniature version of the application containing only what one test needs.

The testing module's three steps

Building the module is always the same sequence:

1const module = await Test.createTestingModule({
2  providers: [
3    LegionsService,
4    { provide: getRepositoryToken(Legion), useValue: repoMock },
5  ],
6}).compile();
7
8const service = module.get<LegionsService>(LegionsService);

Step one:

Test.createTestingModule({...})
creates an isolated module for the test. In
providers
you list only what the test genuinely needs - the service under test and its dependencies. Here we swap the real repository for a double via
useValue
, so the test never touches a database.

Step two:

.compile()
finalises the configuration - NestJS resolves the dependencies between providers and creates their instances. It is an asynchronous operation, hence the
await
. Without this call you hold merely a description of a module, not a working one.

Step three:

module.get(LegionsService)
takes the provider instance out of the finished module - the very one NestJS built, with the repository double already injected. The
<LegionsService>
notation is a type parameter; thanks to it TypeScript knows what you are getting and will suggest its methods.

Remember the three as one: build, compile, take out. Skipping

compile()
is the most common mistake with a first testing module.

A fresh fort before every trial

The code above usually goes into a

beforeEach
hook:

1describe('LegionsService', () => {
2  let service: LegionsService;
3  let repoMock: { find: jest.Mock };
4
5  beforeEach(async () => {
6    repoMock = { find: jest.fn() };
7
8    const module = await Test.createTestingModule({
9      providers: [
10        LegionsService,
11        { provide: getRepositoryToken(Legion), useValue: repoMock },
12      ],
13    }).compile();
14
15    service = module.get<LegionsService>(LegionsService);
16  });
17});

beforeEach
runs before every test, so each one gets a fresh service instance and a clean double. Why, when building the module costs time?

Because without it the tests start affecting one another. A service that stored something in memory during one test carries that state into the next - and then the result begins to depend on the order of execution. Such a test can be green on your machine and red in CI, where Jest parallelises differently. A fresh instance removes that whole class of problems with one line.

One test, one behaviour

With the module in place, we write the actual tests:

1it('returns the list of legions', async () => {
2  repoMock.find.mockResolvedValue([{ id: 1, name: 'Legio X' }]);
3
4  const result = await service.findAll();
5
6  expect(result).toHaveLength(1);
7});
8
9it('returns an empty list when there are no legions', async () => {
10  repoMock.find.mockResolvedValue([]);
11
12  const result = await service.findAll();
13
14  expect(result).toEqual([]);
15});

Two behaviours, two tests - and this is a rule I recommend holding to strictly, @name. It is tempting to check both cases in one

it
, but then you lose the most valuable property of unit tests: when one fails, you know exactly what. A test examining five things at once says as much as a catapult that will not fire - something is wrong, go find it yourself.

Note also what these tests do not check: whether the repository really fetches data from a database. That is deliberate - the double cuts the database off, so we examine the service's logic alone. We will check real elements working together with a different kind of test.

Summary

The machine is apart and every part examined on its own:

  • a unit test examines the smallest element in isolation from the rest of the system,
  • describe
    groups one element's tests,
    it
    describes one behaviour,
    expect
    states an expectation,
  • the name inside
    it
    says what the element should do - it is what appears in the report after a failure,
  • a NestJS service is not created with
    new
    , because it has injected dependencies - that is what the testing module is for,
  • three steps:
    Test.createTestingModule({providers})
    builds an isolated module,
    .compile()
    finalises it and creates the instances,
    module.get(Service)
    takes out the ready provider,
  • skipping
    .compile()
    is the most common mistake - without it you hold a description of a module, not a module,
  • useValue
    swaps a real dependency for a double, so the test never touches a database,
  • beforeEach
    gives a fresh instance before every test - without it the result starts depending on execution order,
  • one behaviour per test: a test examining five things does not tell you which one broke.

In the next lesson we will check what a unit test by design cannot see - whether the elements really work together. For now remember: build, compile, take out - and then examine one part at a time.

Go to CodeWorlds