Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bug] supertest stops if I use more than 4~5 describe() functions in one test file #839

Open
Yush1nk1m opened this issue May 29, 2024 · 0 comments
Labels

Comments

@Yush1nk1m
Copy link

Yush1nk1m commented May 29, 2024

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.
@Yush1nk1m Yush1nk1m added the bug label May 29, 2024
@Yush1nk1m Yush1nk1m changed the title [fix] DESCRIPTIVE TITLE [bug] supertest stops if I use more than 4~5 describe() functions May 29, 2024
@Yush1nk1m Yush1nk1m changed the title [bug] supertest stops if I use more than 4~5 describe() functions [bug] supertest stops if I use more than 4~5 describe() functions in one test file May 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant