You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Jest+Supertest测试Express+TypeScript时出现listen读取错误

问题:运行Jest与Supertest时,测试用例执行前触发app未定义错误

启动脚本运行服务器一切正常,但运行测试脚本时app为undefined,导致测试还没执行就报错。

背景

  • 首次使用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)           // 触发错误的位置
            .post('/user/register')
            .send({
                firstName: 'Thomas',
                lastName: 'Haek',
                email: 'thomashaek@gmail.com',
                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替代export default app或export const server = app,无效
  • 注释所有中间件和路由,仅导出app,无效

解决方案

问题核心是循环依赖:exitHandler.ts导入了index.ts的server,index.ts依赖server.ts,而server.ts关联的errorHandler.ts又导入了exitHandler.ts,导致模块加载时app未初始化就被调用listen方法。

修复步骤:

  1. 拆分服务器启动逻辑:仅在非测试环境启动服务器,避免测试时触发启动代码
    修改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();
    let server: any = null;
    let httpTerminator: any = null;
    
    // 仅在非测试环境启动服务器
    if (process.env.NODE_ENV !== 'test') {
        server = app.listen(config.server.port, () => {
            logger.info(`Server is running on port: ${config.server.port}`);
        });
        httpTerminator = createHttpTerminator({ server });
    }
    
    export { server, httpTerminator };
    
  2. 修复exitHandler的依赖问题:将server和httpTerminator改为参数传入,避免循环导入
    修改exitHandler.ts:

    import mongoose from 'mongoose';
    import logger from '../logger';
    
    class ExitHandler {
        public async handleExit(code: number, timeout = 5000, server?: any, httpTerminator?: any): 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();
    

    之后在处理退出信号的process.ts中,导入server和httpTerminator并传入给exitHandler.handleExit即可。

  3. 设置测试环境变量:在test.env中添加NODE_ENV=test,确保index.ts不会启动服务器。

修改完成后,循环依赖被打破,测试时app能正常从server.ts导入,测试可以正常执行。


内容的提问来源于stack exchange,提问作者Anisul Haque

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 04:35:49