Testing a gateway
Write the tests: end-to-end specs that start the application, connect socket.io clients to it, and check what every client receives, so that a broadcast reaching the wrong client, a refused adoption that goes through, or a departure nobody hears fails a test.
What you'll learn
- Start a Nest application with its gateways in a test, listening on a port, and close it after each test
- Connect socket.io clients from a test, listening before connecting so nothing sent during the handshake is missed
- Assert on acknowledgements with emitWithAck(), and on what every client received, including the ones that should have received nothing
- Know that a gateway test must wait for events in flight before asserting, and why asserting nothing arrived is a real assertion
The live desk from lesson 2 works, and you checked it by hand: two clients in the Socket mode, watching one hear what the other did. That is exactly what breaks silently. Change client.broadcast.emit to this.server.emit while tidying up and nothing crashes, every acknowledgement is still right, and one screen now shows every new cat twice. Tests that connect real socket.io clients to the running gateway, and check what every client received, catch it. This time the gateway is given and read-only, and you write the tests. Four bugs are planted in copies of it, and your tests pass only when each one fails at least one test.
The application in a test
An end-to-end test of a gateway is the Testing course's e2e setup with one difference: the app must listen, because a socket.io client connects to a port. app.init() is not enough.
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = moduleRef.createNestApplication();
await app.listen(3001);
});
afterEach(async () => {
for (const socket of sockets.splice(0)) socket.disconnect();
await app.close();
});
A fresh app per test keeps one test's visitors and cats from leaking into the next. Disconnect the clients before closing the app, so no client is left holding a connection to a server that no longer exists.
Clients in a test
io() from socket.io-client is the same client a browser uses. Two habits make it reliable in a test.
Listen before connecting. A gateway that welcomes clients in handleConnection sends the welcome right behind its acceptance of the connection, so it arrives before the code after await connect(...) gets to run. A listener added there misses it. Create the socket with autoConnect: false, attach the listeners, then call connect(). The given connect() helper does this, and records everything a client receives with onAny():
socket.onAny((event: string, data: unknown) => visitor.received.push([event, data]));
socket.once('connect', () => resolve(visitor));
socket.connect();
Let events arrive before asserting. An acknowledgement can be awaited, await socket.emitWithAck('cats:add', cat), but a broadcast cannot: nothing tells the other client "that was everything". The given settle() waits briefly so events already on their way arrive. Then assert.
Assert on everyone
The planted bugs are not wrong answers; they are right answers to the wrong people. So a test checks the complete list each client received, with toEqual, including the clients that should have received nothing:
expect(bob.received).toEqual([['kennel:fed', feeding]]);
expect(ada.received).toEqual([]);
expect(ada.received).toEqual([]) looks like it asserts nothing. It is the only assertion that catches an event sent to everyone instead of everyone else. When a test is about what happens after the clients have connected, clear what they received while connecting (ada.received.length = 0) after settle(), so the welcome messages do not clutter the expectation.
Your task
test/cats.gateway.e2e-spec.ts has the helpers and one TODO per test. The gateway, as in lesson 2, has Tom and Luna, welcomes each visitor with { visitors }, tells the others desk:joined, sends cats:added to everyone but the sender, cats:adopted with { id, name, by } to everyone, refuses a second adoption with { error: 'Tom was already adopted' }, and tells everyone desk:left when someone goes.
beforeEachstarts a fresh application listening on port 3001;afterEachdisconnects every visitor and closes it.welcomes a visitor with the number of visitors.tells the others who joined, and not the newcomer.acknowledges a new cat, and tells only the others: Milo, age 5, is id 3.tells everyone who adopted a cat, and refuses a second adoption.tells the others who left, anddesk:visitorsno longer lists them.
Run the tests, then read which bugs your tests caught; each bug missed is a client whose received list no test checks.
When it fails
xhr poll errorfromconnect(): the app is not listening, or listens on another port than the clients use.- The welcome is expected and an empty list received: the listener was attached after
await connect(...), and the welcome had already arrived. Use the helper, which listens first. - A test passes alone and fails after another: the app or its clients outlived the test; disconnect and close in
afterEach. - A bug escaped: a test checked the sender but not the other client, or the other client but not the sender.
Remember
- A gateway e2e test listens on a port; disconnect clients before closing the app.
- Attach listeners before connecting, and wait for events in flight before asserting.
- Assert every client's received list, empty ones included.
Stuck? Show a hint
beforeEach: const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = moduleRef.createNestApplication(); await app.listen(3001). afterEach: disconnect each visitor, await app.close(). Each test: connect the visitors it needs, await settle(), clear what was received so far if the test is about what comes next, act (emitWithAck, disconnect), await settle(), then expect every visitor's received list with toEqual, empty lists included.