Home > Back-end >  TypeError: Cannot read properties of undefined (reading 'listen') when testing with Jest,
TypeError: Cannot read properties of undefined (reading 'listen') when testing with Jest,

Time:01-08

Problem:

when running jest and supertest, i get an error before it reaches the actual tests i've described. Server runs fine using the start script, app is defined. But when running test script, app is undefined.

Background:

  • I'm fairly new to typescript and this is the first time I'm using any kind of testing.
  • I want to seperate the server instance, as seen on several blog posts and tutorials, as i plan on running multiple tests in different files.

If you have any suggestions even if they are something I already tried, ill try again and let you know. I am at my wits end so any help is much apprecitated. Thank you.

Error:

 FAIL  src/components/users/user.test.ts
  ● Test suite failed to run

    TypeError: Cannot read properties of undefined (reading 'listen')

       6 |
       7 | dbConnection();
    >  8 | export const server = app.listen(config.server.port, () => {
         |                           ^
       9 |     logger.info(`Server is running on port: ${config.server.port}`);
      10 | });
      11 |

      at Object.<anonymous> (src/index.ts:8:27)
      at Object.<anonymous> (src/library/exitHandler/exitHandler.ts:2:1)
      at Object.<anonymous> (src/library/errorHandler/errorHandler.ts:2:1)
      at Object.<anonymous> (src/middleware/validateSchema.ts:3:1)
      at Object.<anonymous> (src/components/users/routes.ts:2:1)
      at Object.<anonymous> (src/server.ts:2:1)
      at Object.<anonymous> (src/components/users/user.test.ts:2:1)

user.test.ts

import request from 'supertest';
import app from '../../server';

describe('User registration', () => {
    it('POST /register --> return new user instance', async () => {
        await request(app)           // error occurs when reaching this point
            .post('/user/register')
            .send({
                firstName: 'Thomas',
                lastName: 'Haek',
                email: '[email protected]',
                password: '12345678aA',
                confirmPassword: '12345678aA'
            })
            .expect(201)
            .then((response) => {
                expect(response.body).toEqual(
                    expect.objectContaining({
                        _id: expect.any(String),
                        firstName: expect.any(String),
                        lastName: expect.any(String),
                        email: expect.any(String),
                        token: expect.any(String)
                    })
                );
            });
    });
});

server.ts

import express, { Application } from 'express';
import userRouter from './components/users/routes';
import { routeErrorHandler } from './middleware/errorHandler';
import httpLogger from './middleware/httpLogger';
import './process';

const app: Application = express();

app.use(httpLogger);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use('/user', userRouter);
app.use(routeErrorHandler);

export default app

index.ts

import { createHttpTerminator } from 'http-terminator';
import config from './config/config';
import dbConnection from './config/dbConnection';
import logger from './library/logger';
import app from './server'

dbConnection();
export const server = app.listen(config.server.port, () => {
    logger.info(`Server is running on port: ${config.server.port}`);
});

export const httpTerminator = createHttpTerminator({ server });

package.json scripts

"scripts": {
    "test": "env-cmd -f ./src/config/test.env jest --watchAll",
    "start": "env-cmd -f ./src/config/dev.env node build/index.js",

  },

tsconfig.json

{
    "compilerOptions": {
        "outDir": "./build",
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true,
        "module": "commonjs",
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "target": "es6",
        "noImplicitAny": true,
        "moduleResolution": "node",
        "sourceMap": true,
    },
    "include": ["src/**/*"]
}

jest.config.ts

import { Config } from 'jest';

/** @type {import('ts-jest').JestConfigWithTsJest} */
const config: Config = {
    preset: 'ts-jest',
    testEnvironment: 'node',
    roots: ['./src'],
    moduleFileExtensions: ['js', 'ts'],
    clearMocks: true,
    collectCoverage: true,
    coverageDirectory: 'coverage',
    coveragePathIgnorePatterns: ['/node_modules/', '/src/config/'],
    coverageProvider: 'v8',
    coverageReporters: ['json', 'text', 'lcov', 'clover'],
    verbose: true
};

export default config;

exitHandler.ts

import mongoose from 'mongoose';
import { httpTerminator, server } from '../..';


import logger from '../logger';

class ExitHandler {
    public async handleExit(code: number, timeout = 5000): Promise<void> {
        try {
            logger.info(`Attempting graceful shutdown with code: ${code}`);
            setTimeout(() => {
                logger.info(`Forcing a shutdown with code: ${code}`);
                process.exit(code);
            }, timeout).unref();

            if (server.listening) {
                logger.info('Terminating HTTP connections');
                await httpTerminator.terminate();
                await mongoose.connection.close();
            }
            logger.info(`Exiting gracefully with code ${code}`);
            process.exit(code);
        } catch (error) {
            logger.error(error);
            logger.error(
                `Unable to shutdown gracefully... Forcing exit with code: ${code}`
            );
            process.exit(code);
        }
    }
}

export const exitHandler = new ExitHandler();

Things I've tried:

  • using the same env files for both test and server script (same error)
  • messing around with the tsconfig and jest config files (same error)
  • using module.exports = app instead of export default app or export const server = app (same error)
  • commenting out all the middleware and routes and just exporting the app (same error)

CodePudding user response:

I believe this is caused by circular dependency. From the error stack

  at Object.<anonymous> (src/index.ts:8:27)
  at Object.<anonymous> (src/library/exitHandler/exitHandler.ts:2:1
  …
  at Object.<anonymous> (src/server.ts:2:1)
  at Object.<anonymous> (src/components/users/user.test.ts:2:1)

I see that server.ts deps on exitHandler.ts which in turn deps on index.ts. But in index.ts you import app from './server' forming a circle.

More specifically, in order for app from server.ts to be created, it needs exitHandler, but that thing needs index, and index needs server. It’s like recursion without a base case return. Unlike indefinite function recursion, dependency resolution will just give you app as undefined.

Thus you need to break the circle. Use some dependency injection trick to break the tie between exitHandler and index will do.

If you don’t know how to do that, post the exitHandler.ts code and I’ll follow up.


Instead of import { httpTerminator, server } from '../..'; try this:

let server, httpTerminator;

export function injectDependency(s, h) {
  server = s;
  httpTerminator = h;
}

Now in index.ts

import { injectDependency } from "…/exitHandler"

injectDependency(server, httpTerminator);
  • Related