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.
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.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.
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:
creates an isolated module for the test. In Test.createTestingModule({...})
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:
finalises the configuration - NestJS resolves the dependencies between providers and creates their instances. It is an asynchronous operation, hence the .compile()
await. Without this call you hold merely a description of a module, not a working one.Step three:
takes the provider instance out of the finished module - the very one NestJS built, with the repository double already injected. The module.get(LegionsService)
<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.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.
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.
The machine is apart and every part examined on its own:
describe groups one element's tests, it describes one behaviour, expect states an expectation,it says what the element should do - it is what appears in the report after a failure,new, because it has injected dependencies - that is what the testing module is for,Test.createTestingModule({providers}) builds an isolated module, .compile() finalises it and creates the instances, module.get(Service) takes out the ready provider,.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,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.