node.js - 类型错误 : Cannot read properties of undefined (reading 'listen' ) when testing with Jest, super 测试、Express、Typescript

标签 node.js typescript express jestjs supertest

问题:

运行 jest 和 supertest 时,我在达到我描述的实际测试之前收到错误。服务器使用启动脚本运行良好,应用程序已定义。但运行测试脚本时,app 未定义。

背景:

  • 我对 TypeScript 还很陌生,这是我第一次使用任何类型的测试。
  • 我想分离服务器实例,如几篇博客文章和教程中所示,因为我计划在不同的文件中运行多个测试。

如果您有任何建议,即使我已经尝试过,我也会再试一次并让您知道。我束手无策,所以非常感谢任何帮助。谢谢。

错误:

 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: '<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="a1d5c9ceccc0d2c9c0c4cae1c6ccc0c8cd8fc2cecc" rel="noreferrer noopener nofollow">[email protected]</a>',
                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": {
    "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();

我尝试过的事情:

  • 对测试和服务器脚本使用相同的环境文件(相同的错误)
  • 搞乱 tsconfig 和 jest 配置文件(同样的错误)
  • 使用 module.exports = app 而不是导出默认应用程序或导出 const server = app(相同的错误)
  • 注释掉所有中间件和路由并仅导出应用程序(相同的错误)

最佳答案

我相信这是由循环依赖引起的。从错误堆栈中

  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)

我发现 server.ts 依赖于 exitHandler.ts,而 exitHandler.ts 又依赖于 index.ts。但在 index.ts 中,您从“./server”导入应用形成一个圆圈。

更具体地说,为了从 server.ts 创建 app,它需要 exitHandler,但那个东西需要 索引,并且索引需要服务器。这就像没有基本情况返回的递归。与不定函数递归不同,依赖解析只会为您提供 app 作为 undefined

因此你需要打破这个循环。使用一些依赖注入(inject)技巧来打破 exitHandlerindex 之间的联系即可。

如果您不知道如何执行此操作,请发布 exitHandler.ts 代码,我会跟进。


不要使用 import { httpTerminator, server } from '../..'; 试试这个:

let server, httpTerminator;

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

现在在index.ts

import { injectDependency } from "…/exitHandler"

injectDependency(server, httpTerminator);

关于node.js - 类型错误 : Cannot read properties of undefined (reading 'listen' ) when testing with Jest, super 测试、Express、Typescript,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/75042451/

相关文章:

typescript - 如何设置通用约束以确保两种类型之间的公共(public)键也具有相同的值类型?

javascript - webpack注入(inject)脚本后如何加载脚本标签?

node.js - ExpressJS 和 MongooseJS : can I query array values in a subdocument?

node.js - 在 Express Node.js 中禁用 etag header

javascript - 这种 JavaScript 函数声明方式叫什么? (var) => {}

javascript - 我无法在 Node.js 中的 readFile() 回调中更新对象属性

javascript - 如何在 Typescript 中获取自动创建的 Javascript 变量?

node.js - Handlebars 模板引擎循环问题

javascript - 在 nodejs SQLite3 .each() 方法中,如何添加多个参数以处理多个 "WHERE ... = ?"查询?

javascript - 如果 s3 中不存在项目,AWS SDK 会使 Node 应用程序崩溃