Description
Describe the bug
Node.js version: v20.9.0
OS version: Ubuntu 22.04
Description: Test cannot progress if there're more than 4~5 describe()
in a test file.
Actual behavior
I just wrote normal tests which should be passed. I organized tests for one API route into one describe()
function.
If I use more than 4~5 describe()
functions, it is seen that all test()
have been executed and passed but it cannot exit. Like it is fallen into deadlock problem.
My Controller Code
const bcrypt = require("bcrypt");
const passport = require("passport");
const { sequelize, User } = require("../models");
// [u-01] 회원 정보 조회
exports.getUserInfo = (req, res, next) => {
try {
const { userId, email, nickname } = req.user;
return res.status(200).json({ userId, email, nickname });
} catch (error) {
next(error);
}
};
// [u-02] 회원 가입
exports.join = async (req, res, next) => {
try {
const transaction = await sequelize.transaction();
const { userId, email, nickname, password, confirmPassword } = req.body;
if (password !== confirmPassword) {
return res.status(400).send("비밀번호와 확인 비밀번호가 일치하지 않습니다.");
}
const exUser = await User.findOne({ where: { userId } });
if (exUser) {
return res.status(409).send("이미 존재하는 회원 ID입니다.");
}
const hashedPassword = await bcrypt.hash(password, 12);
await User.create({
userId,
email,
nickname,
password: hashedPassword,
}, {
transaction,
});
await transaction.commit();
return res.status(200).send("회원 가입에 성공했습니다.");
} catch (error) {
await transaction.rollback();
return next(error);
}
};
// [u-03] 로그인
exports.login = (req, res, next) => {
passport.authenticate("local", (authError, user, info) => {
if (authError) {
console.error(authError);
return next(authError);
}
if (!user) {
return res.status(400).send("사용자 정보가 일치하지 않습니다.");
}
return req.login(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.status(200).send("로그인에 성공했습니다.");
});
})(req, res, next); // 미들웨어 내의 미들웨어에는 (req, res, next)를 붙인다.
};
// [u-04] 회원 정보 수정
exports.modifyUserInfo = async (req, res, next) => {
try {
const transaction = await sequelize.transaction();
const { userId, nickname } = req.user;
const { newNickname, newPassword, newConfirmPassword, password } = req.body;
const isPasswordCorrect = await bcrypt.compare(password, req.user.password);
if (!isPasswordCorrect) {
return res.status(400).send("비밀번호가 일치하지 않습니다.");
}
if (!newPassword && newNickname === nickname) {
return res.status(400).send("변경될 정보가 존재하지 않습니다.");
}
if (newPassword && newPassword !== newConfirmPassword) {
return res.status(400).send("변경할 비밀번호와 확인 비밀번호가 일치하지 않습니다.");
}
const user = await User.findOne({ where: { userId } });
if (newNickname)
user.nickname = newNickname;
if (newPassword)
user.password = await bcrypt.hash(newPassword, 12);
await user.save({ transaction });
await transaction.commit();
return res.status(200).send("회원 정보가 수정되었습니다.");
} catch (error) {
await transaction.rollback();
next(error);
}
};
// [u-05] 회원 탈퇴
exports.deleteUserInfo = async (req, res, next) => {
try {
const transaction = await sequelize.transaction();
const { confirmMessage } = req.body;
if (confirmMessage !== "회원 탈퇴를 희망합니다.") {
return res.status(400).send("확인 메시지가 잘못되었습니다.");
}
await User.destroy({
where: {
userId: req.user.userId,
},
force: true,
transaction,
});
await transaction.commit();
req.logout(() => {
return res.status(200).send("회원 탈퇴가 완료되었습니다.");
});
} catch (error) {
await transaction.rollback();
next(error);
}
};
// [u-06] 로그아웃
exports.logout = (req, res) => {
req.logout(() => {
return res.status(200).send("로그아웃에 성공하였습니다.");
});
};
My supertest code
jest.mock("openai", () => {
return jest.fn().mockImplementation(() => {
return {
chat: {
completions: {
create: jest.fn().mockImplementation(async () => {
return { choices: [{ message: { content: `{}` } }]};
})
}
}
};
});
});
jest.mock("../services/openai");
const { analysisDiary, analysisMainEmotion } = require("../services/openai");
analysisDiary.mockReturnValue({ emotions: ["기쁨", "사랑", "뿌듯함"], positiveScore: 50, negativeScore: 50 });
analysisMainEmotion.mockReturnValue({ emotion: "기쁨" });
const request = require("supertest");
const app = require("../app");
const { sequelize } = require("../models");
const { joinUserInfo, loginUserInfo, gottenUserInfo, newJoinUserInfo, wrongLoginUserInfo, correctModifyInfo, wrongPasswordModifyInfo, wrongSameModifyInfo, wrongConfirmPasswordModifyInfo, wrongSamePasswordModifyInfo, loginNewUserInfo } = require("../data/user");
jest.setTimeout(2000);
beforeAll(async () => {
await sequelize.sync({ force: true });
});
afterAll(async () => {
await sequelize.sync({ force: true });
});
// [u-01] GET /users
describe("[u-01] GET /users", () => {
const agent = request.agent(app);
// 모든 테스트 시작 전: 회원 가입
beforeAll(async () => {
await request(app).post("/users").send(joinUserInfo);
});
// 각 테스트 시작 전: 로그인
beforeEach(async () => {
await agent.post("/users/login").send(loginUserInfo);
});
// 각 테스트 종료 후: 로그아웃
afterEach(async () => {
await agent.post("/users/logout");
});
// 모든 테스트 종료 후: 회원 탈퇴
afterAll(async () => {
await agent.post("/users/login").send(loginUserInfo);
await agent.delete("/users").send({ confirmMessage: "회원 탈퇴를 희망합니다." });
});
test("[uit-01-1] 로그인되지 않은 상태에서 회원 정보 조회 요청", async () => {
const response = await request(app).get("/users");
expect(response.status).toBe(403);
expect(response.text).toBe("로그인이 필요합니다.");
});
test("[uit-01-2] 성공적인 회원 정보 조회 요청", async () => {
const response = await agent.get("/users");
expect(response.status).toBe(200);
expect(response.body).toEqual(gottenUserInfo);
});
});
// [u-02] POST /users
describe("[u-02] POST /users", () => {
const agent = request.agent(app);
// 모든 테스트 시작 전: 회원 가입
beforeAll(async () => {
await request(app).post("/users").send(joinUserInfo);
});
// 각 테스트 시작 전: 로그인
beforeEach(async () => {
await agent.post("/users/login").send(loginUserInfo);
});
// 각 테스트 종료 후: 로그아웃
afterEach(async () => {
await agent.post("/users/logout");
});
// 모든 테스트 종료 후: 회원 탈퇴
afterAll(async () => {
await agent.post("/users/login").send(loginUserInfo);
await agent.delete("/users").send({ confirmMessage: "회원 탈퇴를 희망합니다." });
});
test("[uit-02-1] 로그인된 상태에서 회원 가입 요청", async () => {
const response = await agent.post("/users");
expect(response.status).toBe(409);
expect(response.text).toBe("이미 로그인된 상태입니다.");
});
test("[uit-02-2] 중복된 회원 ID로 회원 가입 요청", async () => {
const response = await request(app).post("/users").send(joinUserInfo);
expect(response.status).toBe(409);
expect(response.text).toBe("이미 존재하는 회원 ID입니다.");
});
test("[uit-02-3] 성공적인 회원 가입 요청", async () => {
const response = await request(app).post("/users").send(newJoinUserInfo);
expect(response.status).toBe(200);
expect(response.text).toBe("회원 가입에 성공했습니다.");
});
});
// [u-03] POST /users/login
describe("[u-03] POST /users/login", () => {
const agent = request.agent(app);
// 모든 테스트 시작 전: 회원 가입
beforeAll(async () => {
await request(app).post("/users").send(joinUserInfo);
});
// 각 테스트 시작 전: 로그인
beforeEach(async () => {
await agent.post("/users/login").send(loginUserInfo);
});
// 각 테스트 종료 후: 로그아웃
afterEach(async () => {
await agent.post("/users/logout");
});
// 모든 테스트 종료 후: 회원 탈퇴
afterAll(async () => {
await agent.post("/users/login").send(loginUserInfo);
await agent.delete("/users").send({ confirmMessage: "회원 탈퇴를 희망합니다." });
});
test("[uit-03-1] 부정확한 회원 정보로 로그인 요청", async () => {
const response = await request(app).post("/users/login").send(wrongLoginUserInfo);
expect(response.status).toBe(400);
expect(response.text).toBe("사용자 정보가 일치하지 않습니다.");
});
test("[uit-03-2] 이미 로그인되어 있는 상태에서 로그인 요청", async () => {
const response = await agent.post("/users/login").send(loginUserInfo);
expect(response.status).toBe(409);
expect(response.text).toBe("이미 로그인된 상태입니다.");
});
test("[uit-03-3] 성공적인 로그인 요청", async () => {
const response = await request(app).post("/users/login").send(loginUserInfo);
expect(response.status).toBe(200);
expect(response.text).toBe("로그인에 성공했습니다.");
});
});
// [u-04] PATCH /users
describe("[u-04] PATCH /users", () => {
const agent = request.agent(app);
// 모든 테스트 시작 전: 회원 가입
beforeAll(async () => {
await request(app).post("/users").send(joinUserInfo);
});
// 각 테스트 시작 전: 로그인
beforeEach(async () => {
await agent.post("/users/login").send(loginUserInfo);
});
// 각 테스트 종료 후: 로그아웃
afterEach(async () => {
await agent.post("/users/logout");
});
// 모든 테스트 종료 후: 회원 탈퇴
afterAll(async () => {
await agent.post("/users/login").send(loginUserInfo);
await agent.delete("/users").send({ confirmMessage: "회원 탈퇴를 희망합니다." });
});
test("[uit-04-1] 로그인되지 않은 상태에서 회원 정보 수정 요청", async () => {
const response = await request(app).patch("/users").send(correctModifyInfo);
expect(response.status).toBe(403);
expect(response.text).toBe("로그인이 필요합니다.");
});
test("[uit-04-2] 일치하지 않는 비밀번호로 회원 정보 수정 요청", async () => {
const response = await agent.patch("/users").send(wrongPasswordModifyInfo);
expect(response.status).toBe(400);
expect(response.text).toBe("비밀번호가 일치하지 않습니다.");
});
test("[uit-04-3] 부정확한 확인 비밀번호로 회원 정보 수정 요청", async () => {
const response = await agent.patch("/users").send(wrongConfirmPasswordModifyInfo);
expect(response.status).toBe(400);
expect(response.text).toBe("변경할 비밀번호와 확인 비밀번호가 일치하지 않습니다.");
});
test("[uit-04-4] 성공적인 회원 정보 수정 요청", async () => {
const agent = request.agent(app);
await agent.post("/users").send(newJoinUserInfo);
await agent.post("/users/login").send(loginNewUserInfo);
const response = await agent.patch("/users").send(correctModifyInfo);
expect(response.status).toBe(200);
expect(response.text).toBe("회원 정보가 수정되었습니다.");
});
});
Expected behavior
All test needs to be passed.
Actually, If I shuffle the order of describe()
s, supertest always stops at specific timimg, not a specific test.
But, if I write describe("[u-04] PATCH /users", ...)
to another file and execute test, this problem is solved.
I have tried to add many options like --detectOpenHandle
, --runInBand
or --forceExit
. But it couldn't work when there're more than 4~5 describe()
s. And the only way to solve this problem was writing a new test file
.
And I also set test time to 60 seconds but the test always stopped If there're more than 4~5 describe()
functions.
Isn't it weird? My code runs sequentially so there's no room for deadlock. I guess it is caused by describe()
's implementation.
If you have some time, could you check it sir?
Code to reproduce
Checklist
- [ V ] I have searched through GitHub issues for similar issues.
- [ V ] I have completely read through the README and documentation.
- [ V ] I have tested my code with the latest version of Node.js and this package and confirmed it is still not working.