Course & Homework
- Course: AI Dev Tools Zoomcamp (2026)
- Module: Homework 2 - Build and Ship an AI-Assisted Full-Stack App
Problem Description
When running integration tests for a collaborative full-stack application using Node.js's built-in test runner (node --test) against an Express and Socket.io server, the test process hangs indefinitely in the terminal after executing, or fails with ERR_SERVER_ALREADY_LISTEN.
Root Cause
- Premature port binding: If
server.listen(PORT) is executed at the top level of server/index.js, importing the app/server into tests/integration.test.js causes the HTTP server to bind before the test hook can bind to an ephemeral port (0).
- Open handles: Socket.io and
http.Server maintain persistent event loop connections and internal heartbeat timers, preventing Node's test runner from detecting an empty event loop.
Solution & Best Practice
-
Guard server.listen in server/index.js:
Ensure the server only listens when executed directly, not when imported by test suites:
if (require.main === module) {
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
}
-
Explicitly close both io and server in the test cleanup hook:
after(async () => {
io.close();
await new Promise((resolve) => serverInstance.close(resolve));
});
-
Use --test-force-exit in package.json:
Add --test-force-exit so Node exits cleanly once all tests pass:
"scripts": {
"test": "node --test --test-force-exit tests/*.test.js"
}
Course & Homework
Problem Description
When running integration tests for a collaborative full-stack application using Node.js's built-in test runner (
node --test) against an Express and Socket.io server, the test process hangs indefinitely in the terminal after executing, or fails withERR_SERVER_ALREADY_LISTEN.Root Cause
server.listen(PORT)is executed at the top level ofserver/index.js, importing the app/server intotests/integration.test.jscauses the HTTP server to bind before the test hook can bind to an ephemeral port (0).http.Servermaintain persistent event loop connections and internal heartbeat timers, preventing Node's test runner from detecting an empty event loop.Solution & Best Practice
Guard
server.listeninserver/index.js:Ensure the server only listens when executed directly, not when imported by test suites:
Explicitly close both
ioandserverin the test cleanup hook:Use
--test-force-exitinpackage.json:Add
--test-force-exitso Node exits cleanly once all tests pass: