Files
What you'll learn
- Read a failing test as a specification and implement the smallest code that satisfies it
- Keep unit and e2e specs in step: the service's contract in one, the route's in the other
- Take the workflow home: how Jest, the Nest CLI's spec files and npm test relate to what ran here
Test First
Every lesson in this course so far had the code and asked for the tests. Turn it round. Two spec files describe a feature the cats API does not have: cats can be updated with PATCH /cats/:id, and GET /cats?minAge=4 lists only the older ones. The tests fail, because nothing implements them. Your job is to make them pass, and nothing more. This is the workflow called test-driven development, and whether or not you adopt it as a habit, doing it once shows what a test is for: it is the specification, written in the only language that can check itself.
Red, green, refactor
The loop has three steps and the order matters:
- Red. Write a test for the next small piece of behaviour and run it. It fails, and the failure says what is missing:
service.update is not a function,expected 200 "OK", got 404 "Not Found". A test that passes before the code exists is testing nothing; seeing it red first proves it can fail. - Green. Write the least code that makes it pass. Not the best code, the least: the point is to get back to a state where everything is known to work, in minutes, not hours.
- Refactor. With everything green, improve the code: remove duplication, name things, move logic where it belongs. The tests say whether the refactor changed behaviour, which is the whole reason to have them before refactoring rather than after.
Then the next test. Working in small loops keeps the distance between "it worked" and "it does not" to a few lines, which is where bugs are cheap.
Reading a failing test
A test is a sentence: update › changes only the fields it is given, with the arrangement (three cats), the act (service.update(1, { age: 4 })) and the expected result ({ id: 1, name: 'Tom', age: 4 }). Read the three parts before writing anything: the arrangement says what state to assume, the act says the signature, the assertion says the return value and, in the next test, that the change is stored. The e2e file adds what the unit file cannot: the route is PATCH, the body is validated (a negative age is a 400 with that exact message), a missing cat is a 404, and the filter arrives as a query string that must be a number.
Two spec files, one feature, is the shape to keep. The unit spec pins the service's contract, fast and precise; the e2e spec pins the route's, including what the pipes and the decorators add. When both exist, a change that breaks either is caught by the one that can name it.
Taking it home
Everything that ran here runs on your machine unchanged. nest new sets up Jest; nest generate service cats writes cats.service.spec.ts beside the service with a Test.createTestingModule already in it; npm run test runs the unit specs, npm run test:e2e the files under test/ matching *.e2e-spec.ts, and npm run test:cov reports coverage. supertest is the real package there, and request(app.getHttpServer()) opens a real socket to the same application. The describe, it, expect and jest you used are Jest's; the matchers here are the everyday subset, and Jest has more (toMatchSnapshot, toHaveBeenCalledBefore, custom matchers) when you need them.
What no tool does for you is the judgement: what to test. Test behaviour a caller depends on, at the boundary where they depend on it; test the error paths, which are where bugs hide; test the thing that just broke, so it stays fixed. Do not test that a class has a method, that a decorator is present, or how a method does its work, because those tests break on every refactor and catch nothing.
Your task
Both spec files are complete and both fail. Make them pass:
CatsService.update(id, changes)changes the fields it is given, keeps the rest, stores the result, returns the cat, and throws the usualNotFoundExceptionfor a missing cat.findAll(filter)keeps only cats whose age is at leastminAge, everything when there is no filter.CatsControllergetsPATCH /cats/:idtaking anUpdateCatDto, andGET /catsreads an optional numericminAgefrom the query string, defaulting to zero, rejecting anything that is not a number.
Run after each change and read the next failure. UpdateCatDto is written; DefaultValuePipe and ParseIntPipe are imported.
When it fails
service.update is not a function: red, as expected. That is the first thing to write.changes only the fields it is givenfails withname: undefined: the update replaces the cat with the changes instead of merging them.Object.assign(cat, changes)merges; the DTO's undefined fields are absent, notundefined, becausewhiteliststrips them.GET /cats?minAge=abc is a 400fails with 200: the query value is read withoutParseIntPipe, so'abc'becameNaNand nothing rejected it.PATCH /cats/:id changes the fieldsfails with 404: the route is missing, or declared as@Put. The test sayspatch.
Remember
- Red, green, refactor: see the test fail, make it pass with the least code, then improve with the tests as a net.
- A failing test is a specification: arrangement, act, assertion say what to build.
- Unit specs pin the class, e2e specs pin the route; keep both for a feature.
- The same code runs under Jest on your machine:
nest generatewrites the spec files,npm run testandtest:e2erun them.
Stuck? Show a hint
Service: update() can reuse findOne() for the 404, then Object.assign(cat, changes) and return the cat; findAll(filter) filters by c.age >= (filter?.minAge ?? 0). Controller: @Get() findAll(@Query('minAge', new DefaultValuePipe(0), ParseIntPipe) minAge: number) and @Patch(':id') update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateCatDto).
Press Run tests to start the app. Its log appears here.Tests
- cats/cats.service.spec.tsrun to see its tests
- test/cats.e2e-spec.tsrun to see its tests