Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 31

Automating Server Teardown for Tests

Recommended resources

Introduction

In our last lesson, you successfully set up Jest and Supertest, refactored the application for testability, and wrote your first integration test using request(app). You might have noticed that the test process started, ran, and exited cleanly without any extra configuration. This is because Supertest, when given an app object, cleverly handles the server startup and shutdown for you behind the scenes.

While convenient, this implicit behavior can be a problem in more complex setups. If a server isn't closed properly after tests, it leaves behind an "open handle"—a running process that prevents Jest from exiting cleanly. This is a common pitfall that can break automated testing workflows.

In this lesson, you will learn the explicit and professional pattern for managing the server lifecycle. We will use Jest's built-in beforeAll and afterAll hooks to guarantee that our test server starts exactly once before our tests run and shuts down cleanly afterward. This will ensure your test suite is robust, efficient, and free of resource leaks, a critical skill for production-level engineering and your exam.


1. The "Open Handles" Problem

When you run tests, especially with the --detectOpenHandles flag, Jest scans for any processes that haven't been terminated. An Express server listening on a port is a prime example. If it's still running after the tests complete, Jest will report it and may hang.

Jest Test Output with Leak and Handle Detection
This image shows Jest being run with the `--detectOpenHandles` flag. This flag is an essential diagnostic tool for finding resource leaks, like an unclosed server, in your test environment.

To prevent this, we must ensure our server is explicitly closed.


2. Using beforeAll and afterAll for Server Lifecycle Management

Jest provides "hooks" that allow you to run setup and teardown code. For managing a server, the two most important are:

  • beforeAll(fn): Runs a function once before any of the tests in a file execute. This is the perfect place to start our server.
  • afterAll(fn): Runs a function once after all of the tests in a file have completed. This is where we will close our server.

This approach is more explicit and gives you complete control over the test environment.


3. Implementing Explicit Server Control

Let's refactor your productRoutes.test.js file to incorporate this pattern. You will need to make a few key changes to how the server is created and how Supertest makes requests.

  1. Import Node's http module: We need it to create a server instance from our app object.
  2. Declare a server variable: This will hold our server instance so we can access it in both beforeAll and afterAll.
  3. Implement beforeAll: This hook will create the server using http.createServer(app) and start it. We'll use the done callback to signal when the asynchronous listen operation is complete.
  4. Implement afterAll: This hook will call server.close(), again using done to handle the asynchronous shutdown.
  5. Update request() calls: Instead of passing app to Supertest, you will now pass the running server instance.

Here is the updated structure for your src/routes/productRoutes.test.js file. Apply these changes to your code.

const request = require('supertest');
const http = require('http'); // 1. Import http
const app = require('../../app');
const Product = require('../models/productModel');

jest.mock('../models/productModel');

let server; // 2. Declare server variable

// 3. Start server before all tests
beforeAll((done) => {
    server = http.createServer(app);
    server.listen(done);
});

// 4. Close server after all tests
afterAll((done) => {
    server.close(done);
});

describe('Product API', () => {
    
    describe('GET /api/products', () => {

        it('should return all products', async () => {
            const mockProducts = [
                { _id: '1', name: 'Product 1', price: 100 },
                { _id: '2', name: 'Product 2', price: 200 },
            ];
            
            Product.find.mockResolvedValue(mockProducts);

            // 5. Update request to use the server instance
            const res = await request(server).get('/api/products');

            expect(res.statusCode).toEqual(200);
            expect(res.body).toEqual(mockProducts);
            expect(Product.find).toHaveBeenCalledTimes(1);
        });

    });

    // ... your other tests will go here
});

This pattern ensures a clean, isolated test run for each test file. The server starts once, all tests in the file run against it, and then it shuts down.

Unit Testing in NodeJS with Express, TypeScript, Jest and Supertest

The article 'Unit Testing in NodeJS' demonstrates this exact pattern of managing a test server's lifecycle. It's a great resource for seeing this technique in a slightly different context.

First, read the section 'Writing Unit Tests for Routes'. Notice how it emphasizes the need to isolate a testing server. It creates a test_setup.ts file to start and export the server. Then, look at the code block in the next section, which shows how test_server.close() is called within the afterAll hook in userRoutes.test.ts. This reinforces the teardown pattern you just implemented.

By passing the done callback to server.listen(done) and server.close(done), you are telling Jest to wait until these asynchronous operations are finished before moving on. This prevents race conditions where tests might run before the server is ready or where the test process exits before the server has fully shut down.

Run npm test again. Your test should pass just as before, but now you have a more robust and explicit setup that you are in full control of.


Conclusion

You have now mastered a fundamental skill for building professional-grade test suites: explicit lifecycle management. While Supertest's implicit handling is useful for quick tests, using beforeAll and afterAll provides the clarity and control needed for reliable automated testing at scale.

Key Takeaways:

  • Forgetting to close a server leads to "open handles," which can cause Jest to hang.
  • The beforeAll hook is used to perform setup once per test file, like starting a server.
  • The afterAll hook is used for teardown once per test file, ensuring resources like servers are cleanly closed.
  • When managing the server manually, you must pass the server instance to request(), not the app object.
  • Using the done callback in beforeAll and afterAll is essential for handling asynchronous setup and teardown operations.

Next Up:

With a solid setup for both mocking and server management in place, you are now ready to build out the full test suite for your API. In the next lesson, we will systematically write integration tests for all the remaining CRUD operations (POST, GET by ID, PUT, DELETE), focusing on both success cases and critical error handling scenarios like validation failures and "not found" errors.