Tested end to end
Prove the service works the way a client meets it: end-to-end tests that boot the whole application, sign in as staff and as an adopter, and check the rules that matter, so that each of four realistic regressions fails at least one test.
What you'll learn
- Boot the whole service in a test, configure it as main.ts does, and give every test a fresh application and database
- Write a helper that signs in, and test authorization by trying every route as nobody, as an adopter and as staff
- Assert what a response must not contain as well as what it must
- Check a transaction's rollback by reading the data back after a refusal, not only the refusal's status
The shelter service now does a lot, and every lesson's graded requests proved it once. But the requests are this course's, not the service's: the day someone refactors the adoptions module, nothing in the repository says what must still be true. Tests do. This lesson's code is the finished service, given and read-only; you write the end-to-end tests that pin down its rules. Four regressions are planted in copies of it, each the kind a refactor makes, and your tests pass only when every one fails at least one test.
The whole service in a test
An end-to-end test boots the real application, database and guards included, and talks to it over HTTP with supertest, as the Testing course did. The testing module imports AppModule, so every global guard, filter and interceptor registered there with APP_GUARD, APP_FILTER and APP_INTERCEPTOR is in the test too. What is not: anything main.ts does, because main.ts does not run. The global ValidationPipe is configured there, so the test applies it itself, with the same options, before app.init().
A fresh application per test means a fresh in-memory database per test, seeded again with Tom and Luna and the staff account, and a fresh throttle count. Tests then pass in any order, which is the property that makes a failing test worth reading. afterEach closes the application.
Testing who may do what
Authorization bugs are the quiet ones: a removed @Roles() leaves every request working, including the ones that should fail. So a test of a protected route tries it three ways:
await request(server).delete('/kennels/1').expect(401);
await request(server).delete('/kennels/1').set('Authorization', `Bearer ${volunteer}`).expect(403);
await request(server).delete('/kennels/1').set('Authorization', `Bearer ${manager}`).expect(204);
The given signIn() helper registers an adopter, unless it is the seeded sam, and answers a token, which keeps each test about its rule rather than its setup.
Testing what must not be there
A leak is something present that should not be. expect(res.body.username).toBe('ana') passes whether or not the hash came along. Compare the whole body with toEqual, which fails on an extra key, or assert the absence directly.
Testing a rollback
A refused request's status says it was refused, not that nothing changed. A rule checked in the wrong place, after the transaction committed, still answers the 409, while the change it should have prevented stays in the database. The only test that tells the two apart reads the data back after the refusal:
await request(server).post('/kennels/2/book').set('Authorization', `Bearer ${token}`).expect(409);
const kennel = await request(server).get('/kennels/2').expect(200);
expect(kennel.body.occupied).toBe(3);
Your task
test/shelter.e2e-spec.ts has the setup started, the signIn() helper and four tests to write. Tom (id 1, 3, Tabby) and Luna (id 2, 2, Siamese) start in the database; sam with whiskers-staff is staff.
- Complete
beforeEach(whatmain.tswould configure, theninit) andafterEach. lets anyone read the cats, and only staff change them: the list without a token; adding Milo, age 5, is 401 without a token, 403 as an adopter and 201 as staff, with the whole cat in the body.never answers with a password or its hash: registration and/auth/meanswer exactly{ id, username, role }.adopts a cat once: an adoption, then a second adopter's 409.refuses a third adoption, and leaves the cat available: two adoptions, a refused third, the third cat read back as not adopted, and two adoptions in the adopter's list.
Then read which bugs your tests caught. Each bug missed is a rule no test pins down.
When it fails
expected 200 "OK", got 404 "Not Found"for routes that exist: the application was created but never initialised, so no route is registered;await app.init().- Tests pass alone and fail together, with ids off by one: the application outlives a test; close it in
afterEach. an adopter can add catsescaped: the test checked staff's 201 but not the adopter's 403.the limit is checked after the commitescaped: the test checked the 409 but never read the cat back.
Remember
AppModulebrings the global guards, filters and interceptors;main.ts's configuration is the test's to repeat.- A fresh application per test, closed after it.
- Test a protected route as nobody, as the wrong role and as the right one.
- Compare whole bodies to catch leaks; read data back to prove a rollback.
Stuck? Show a hint
beforeEach: Test.createTestingModule({ imports: [AppModule] }).compile(), createNestApplication(), useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })), await app.init(); afterEach: app.close(). Each test: request(app.getHttpServer()) with .set('Authorization', `Bearer ${token}`), .expect(status), then expect() on res.body with toEqual for whole bodies, toBe for messages, and a GET after a refused adoption.