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

移端口配置至app.js后Jest测试出现未退出及端口占用错误

问题描述
  • 测试脚本原本运行正常,将端口配置从www文件迁移到app.js后出现异常:
    • 执行npm test时,8个测试脚本仅4个通过,其余报端口已占用错误;
    • 单独运行失败脚本时,出现Jest did not exit one second after the test run has completed错误;
    • 已尝试--force-exit和--detectOpenHandles参数,均无效。

app.js 代码

var express = module.exports = require("express");
const path = require("path");
var cookieParser = require("cookie-parser");
const http = require("http");
const socketIo = require("socket.io");
var debug = require('debug')('tutormeister-app:server');
 
var app = express();
const session = require("express-session");
 
//Create HTTP server using Express app
const server = http.createServer(app);
 
//Pass the server object to Socket.IO
const io = socketIo(server);
 
//Set up Socket.IO in your app
app.set("io", io);
 
const socketsConnected = new Set();
 
io.on('connection', (socket) => {
socketsConnected.add(socket.id);
io.emit('clients-total', socketsConnected.size);
 
socket.on('disconnect', () => {
socketsConnected.delete(socket.id);
io.emit('clients-total', socketsConnected.size);
  });

socket.on('message', (data) => {
socket.broadcast.emit('chat-message', data);
  });

socket.on('feedback', (data) => {
socket.broadcast.emit('feedback', data);
 });
});
 
var loginRouter = require("./routes/login_router");
var signupRouter = require("./routes/signup_router");
var createCourseRouter = require("./routes/create_course_router");
var insHomeRouter = require("./routes/ins_home_router");
var insViewCourseRouter = require("./routes/ins_view_course_router");
var accountEditRouter = require("./routes/account_edit_router");
var stuHomeRouter = require("./routes/stu_home_router");
var insStudentViewRouter = require("./routes/ins_student_view_router");
var studentMyCourseRouter = require("./routes/studentMyCourse_router");
var existingStudentCourseRouter = require("./routes/existing_student_course_router");
var stuCourseHomeRouter = require("./routes/stu_course_home_router");
var acceptStudentRouter = require("./routes/accept_student_router");
var communityRouter = require("./routes/community_router");

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
 
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
 
app.use(
session({
  secret: "your-secret-key",
  resave: false,
  saveUninitialized: false,
 cookie: { maxAge: 3600000 },
   })
 );
 
app.use((req, res, next) => {
//Helper function to determine active status
res.locals.active = (path) => (req.path === path ? "active" : "");
next();
 });
 
app.use((req, res, next) => {
const isLoginPage = req.path === "/login";
const isLoginPage2 = req.path === "/";
const isInstructorSignupPage = req.path === "/instructor-register";
const isStudentSignupPage = req.path === "/student-register";
 
 
const isLoggedIn = req.session && req.session.instructorId;
 
 if (
     (isLoginPage ||
 isLoginPage2 ||
 isInstructorSignupPage ||
 isStudentSignupPage) &&
 isLoggedIn
   ) {
// Destroy the session only if the user is already logged in and accessing /login, /instructor-register, or /student-register
req.session.destroy((err) => {
if (err) {
console.error("Error destroying session:", err);
       }
// Redirect to the respective page after destroying the session
if (isLoginPage) {
  res.redirect("/login");
else if (isInstructorSignupPage) {
   res.redirect("/instructor-register");
else {
  res.redirect("/student-register");
      }
    });
else {
// Continue to the next middleware or route handler
  next();
  }
});
 
app.get("/", loginRouter);
app.post("/", loginRouter);

app.get("/health", loginRouter);

app.get("/login", loginRouter);
app.post("/login", loginRouter);

app.get("/logout", loginRouter);

app.get("/instructor-register", signupRouter);
app.post("/instructor-register", signupRouter);

app.get("/student-register", signupRouter);
app.post("/student-register", signupRouter);

app.get("/create-course", createCourseRouter);
app.post("/create-course", createCourseRouter);

app.get("/instructor-home", insHomeRouter);
app.post("/instructor-home", insHomeRouter);

app.get("/instructor-account-edit", accountEditRouter);
app.post("/instructor-account-edit", accountEditRouter);

app.get("/student-account-edit", accountEditRouter);
app.post("/student-account-edit", accountEditRouter);

app.get("/viewCourse", insViewCourseRouter);

app.get("/student-home", stuHomeRouter);
app.post("/student-home", stuHomeRouter);

app.get("/studentMyCourse", studentMyCourseRouter);
app.post("/studentMyCourse",studentMyCourseRouter);

app.get("/existingStudentCourse", existingStudentCourseRouter);
app.post("/existingStudentCourse",existingStudentCourseRouter);

app.get("/instructor-student-view", insStudentViewRouter);
app.post("/instructor-student-view", insStudentViewRouter);

app.get("/student-course-home", stuCourseHomeRouter);
app.post("/student-course-home", stuCourseHomeRouter);

app.get("/acceptStudent", acceptStudentRouter);
app.post("/acceptStudent", acceptStudentRouter);
app.post("/rejectStudent", acceptStudentRouter);

app.get("/chat", communityRouter);
 
app.set('port', normalizePort(5000));
 
/**
 Normalize a port into a number, string, or false.
  */
 
function normalizePort(val) {
var port = parseInt(val, 10);
 
if (isNaN(port)) {
  named pipe
  return val;
  }
 
if (port >= 0) {
  port number
  return port;
   }

return false;
}
 
/**
 Event listener for HTTP server "error" event.
 */
 
function onError(error) {
 if (error.syscall !== 'listen') {
 throw error;
   }
var port = normalizePort(app.get('port'));
var bind = typeof port === 'string'
     ? 'Pipe ' + port
     : 'Port ' + port;
 
// handle specific listen errors with friendly messages
switch (error.code) {
 case 'EACCES':
 console.error(bind + ' requires elevated privileges');
 process.exit(1);
 break;
 case 'EADDRINUSE':
 console.error(bind + ' is already in use');
 process.exit(1);
 break;
 default:
 throw error;
   }
 }
 
 /**
 Event listener for HTTP server "listening" event.
  */
 
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
     ? 'pipe ' + addr
     : 'port ' + addr.port;
debug('Listening on ' + bind);
 }
 
 /**
 Start the HTTP server.
  */
 
server.listen(app.get('port'), "0.0.0.0");
server.on('error', onError);
server.on('listening', onListening);

失效的测试脚本代码

const supertest = require('supertest'); const app = require('../app');

// Mocking the database connection
jest.mock('../common/database'); 
const db = require('../common/database'); 
const createCourseQuery = require('../querymanager/createCourseQuery'); 
const createCourseController = require('../backend/create_course_controller'); 
const { validationResult } = require('express-validator');

describe('Create Course Controller', () => { 
  test('createCourse should render "createCourse" view', async () => { 
    const mockSession = { instructorId: 1 }; 
    const mockReq = { session: mockSession }; 
    const mockRes = { render: jest.fn(), redirect: jest.fn() };

    await createCourseController.createCourse(mockReq, mockRes);

    expect(mockRes.render).toHaveBeenCalledWith('createCourse');   
  });

  test('createCoursePost should redirect to "/instructor-home" on successful course creation', async () => { 
    const mockSession = { instructorId: 1 }; 
    const mockConnection = { 
      connect: jest.fn(), 
      query: jest.fn().mockImplementation((query, values, callback) => { 
        callback(null, { insertId: 1 }); // Mocking successful course insertion
      }), 
      end: jest.fn()
    };

    db.getMySQLConnection.mockReturnValueOnce(mockConnection); 
    jest.mock('express-validator', () => ({
      ...jest.requireActual('express-validator'), 
      validationResult: jest.fn(() => ({ isEmpty: true }))
    }));

    const mockReq = { 
      session: mockSession, 
      body: { 
        courseName: 'Math 101', 
        subject: 'Mathematics', 
        eduLevel: 'High School', 
        level: 'Intermediate', 
        teachingStyle: 'Interactive', 
        teachingFormat: 'Online', 
        teachingMode: 'Live', 
        video: 'https://example.com/math101', 
        description: 'Introduction to mathematics.'
      }
    }; 
    const mockRes = { redirect: jest.fn(), render: jest.fn(), send: jest.fn() };

    await createCourseController.createCoursePost(mockReq, mockRes);

    expect(mockConnection.connect).toHaveBeenCalled(); 
    expect(mockConnection.query).toHaveBeenCalledWith( 
      createCourseQuery.ADD_COURSE, 
      [
        'Math 101',
        'Mathematics',
        'High School',
        'Intermediate',
        'Interactive',
        'Online',
        'Live',
        'https://example.com/math101', 
        1, // instructorId
        'Introduction to mathematics.'
      ], 
      expect.any(Function)
    ); 
    expect(mockRes.redirect).toHaveBeenCalledWith('/instructor-home'); 
    expect(mockReq.session.courseId).toBe(1); 
    expect(mockConnection.end).toHaveBeenCalled();   
  });

});
解决方案

1. 避免测试时自动启动服务器

问题核心:每次测试文件require('../app')时都会执行server.listen启动服务器,多文件并行运行导致端口冲突。

修改app.js,将服务器启动逻辑放在条件判断内,仅直接运行app.js时才启动:

// 替换app.js末尾的启动代码
if (require.main === module) {
  server.listen(app.get('port'), "0.0.0.0");
  server.on('error', onError);
  server.on('listening', onListening);
}

// 导出server,方便测试时关闭
module.exports = { app, server };

2. 测试结束后关闭服务器和Socket.IO连接

在测试文件中添加afterAll钩子,确保测试完成后关闭服务器,解决Jest不退出问题:

// 修改测试文件的引入
const { app, server } = require('../app');

// 在describe块外添加钩子
afterAll(async () => {
  await new Promise(resolve => server.close(resolve));
});

3. 修复express-validator的mock问题

测试中动态mockexpress-validator可能导致异常,改用jest.spyOn来mock:

// 在目标测试用例内替换原mock代码
const validationResultSpy = jest.spyOn(require('express-validator'), 'validationResult');
validationResultSpy.mockReturnValue({ isEmpty: true });

// 测试结束后恢复mock
afterEach(() => {
  jest.restoreAllMocks();
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 18:05:57