Skip to content

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

@Yush1nk1m

Description

@Yush1nk1m

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions