移端口配置至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
相关产品推荐
相关产品推荐

