oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
73.55k stars 2.71k forks source link

Restore mock.module using mock.restore not work as expect #7823

Open yukikwi opened 9 months ago

yukikwi commented 9 months ago

What version of Bun is running?

1.0.20+09d51486e

What platform is your computer?

Linux 6.5.0-14-generic x86_64 x86_64

What steps can reproduce the bug?

I create test file with this code inside

import axios from "axios"
import { describe, expect, it, mock } from "bun:test"

describe("topic", () => {
  it("test case 1", async () => {
    // before mock
    expect((await axios("https://google.com/", { method: "GET" })).status).toBe(200)

    // mock
    mock.module("axios", () => ({
      default: () => {
        return {
          status: 500
        } 
      }
    }));

    // after mock
    expect((await axios("https://google.com/", { method: "GET" })).status).toBe(500)

    // restore mock & test again
    mock.restore()
    expect((await axios("https://google.com/", { method: "GET" })).status).toBe(200)
  })
})

What is the expected behavior?

I expect this test case must pass

What do you see instead?

// before mock expect((await axios("https://google.com/", { method: "GET" })).status).toBe(200) -> this pass

// after mock expect((await axios("https://google.com/", { method: "GET" })).status).toBe(500) -> this pass

// restore mock & test again expect((await axios("https://google.com/", { method: "GET" })).status).toBe(200) -> this fail (value is 500)

Additional information

This one work like what I expected but I think it is not good solution

import axios from "axios"
import path from "path"
import { describe, expect, it, mock } from "bun:test"

describe("topic", () => {
  it("test case 1", async () => {
    // before mock
    expect((await axios("https://google.com/", { method: "GET" })).status).toBe(200)

    // mock
    const axiosBak = axios  <-- add this
    mock.module("axios", () => ({
      default: () => {
        return {
          status: 500
        } 
      }
    }));

    // after mock
    expect((await axios("https://google.com/", { method: "GET" })).status).toBe(500)

    // restore mock & test again
    mock.restore()
    mock.module("axios", () => axiosBak) <-- add this
    expect((await axios("https://google.com/", { method: "GET" })).status).toBe(200)
  })
})
chlorophant commented 8 months ago

Not only is the restore not working, modules that are mocked will conflict with other testfiles that also need to mock the given module. Not sure what the path forward is for this but its a major blocker

jpasquers commented 8 months ago

This is the workaround I have been using in the meantime:

import { mock } from "bun:test";

export type MockResult = {
    clear: () => void;
};

/**
 *
 * @param modulePath - the path starting from this files' path.
 * @param renderMocks - function to generate mocks (by their named or default exports)
 * @returns an object
 */
export const mockModule = async (
    modulePath,
    renderMocks: () => Record<string, any>,
): Promise<MockResult> => {
    let original = {
        ...(await import(modulePath)),
    };
    let mocks = renderMocks();
    let result = {
        ...original,
        ...mocks,
    };
    mock.module(modulePath, () => result);
    return {
        clear: () => {
            mock.module(modulePath, () => original);
        },
    };
};

Then for each test suite declare

    let mocks: MockResult[] = [];
    afterEach(() => {
        mocks.forEach((mockResult) => mockResult.clear());
        mocks = [];
    });

Then finally for example in the test

        mocks.push(
            await mockModule("./utils/aws", () => ({
                ensureAwsSsoSession: jest.fn(() => Promise.resolve()),
            })),
        );

I can't stress enough that this is a hacky unstable solution that I am only leveraging because our unit tests would be dead in the water otherwise. Hopefully this helps others until a permanent solution comes through 👍

Jarred-Sumner commented 8 months ago

As a temporary workaround, you can try jest.restoreAllMocks

import {jest} from 'bun:test';

jest.restoreAllMocks();
jpasquers commented 8 months ago

@Jarred-Sumner hmm unfortunately that didn't resolve it for me, or at the very least the way I used it wasn't working. I leveraged it as an afterEach, i.e.

afterEach(() => jest.restoreAllMocks());

And using the standard mock.module (example usage):

        mock.module("./utils/aws", () => ({
            ensureAwsSsoSession: jest.fn(() => Promise.resolve()),
        }));
cuquo commented 8 months ago

I'm facing the same issue with mock.module, I also tried mocking the module with require.resolve('path') in order to see if that would create another mock for the same module but with no luck.

Ideally the mock.module should only be valid inside that test, other tests could mock the same module with other values like jest. Global mocks should be added in the preload file IMHO.

RichAyotte commented 3 months ago

As a temporary workaround, you can try jest.restoreAllMocks

import {jest} from 'bun:test';

jest.restoreAllMocks();

This doesn't work. Any other workarounds other than instantiating a new bun process for every test file?

MonsterDeveloper commented 2 months ago

Since #10210 is closed now, we really need the fix for mock.restore or jest.restoreAllMocks… Right now it’s really hard to use bun’s test runner on more or less big project with module mocking without spawning separate process for every test file.

dmvvilela commented 1 month ago

Any updates on this? When i mock a common module it just breaks everything on other tests.

ziroock commented 1 month ago

I am facing the same issue! Tests break once I mock a function A in a unit test and then use the same function A in an integration test. I have separate files for the unit and integration tests. Mocking a module mocks globally, not on describe() level. I used mock.restore() and jest.restoreAllMocks(), but still nothing. Are there any updates?

aryzing commented 1 month ago

Seems the conversation has shifted a bit towards cross-test module mocking, here's a related issue on that: https://github.com/oven-sh/bun/issues/12823

jstlaurent commented 1 month ago

I ended up using @jpasquers' solution, from this earlier comment. It's unfortunate that it doesn't work out of the box, but the benefits I get from testing outweigh the additional complexity.

I tweaked the solution to use a class, so it's all encapsulated in one place. It looks like this:

/**
 * Due to an issue with Bun (https://github.com/oven-sh/bun/issues/7823), we need to manually restore mocked modules
 * after we're done. We do this by setting the mocked value to the original module.
 *
 * When setting up a test that will mock a module, the block should add this:
 * const moduleMocker = new ModuleMocker()
 *
 * afterEach(() => {
 *   moduleMocker.clear()
 * })
 *
 * When a test mocks a module, it should do it this way:
 *
 * await moduleMocker.mock('@/services/token.ts', () => ({
 *   getBucketToken: mock(() => {
 *     throw new Error('Unexpected error')
 *   })
 * }))
 *
 */
export class ModuleMocker {
  private mocks: MockResult[] = []

  async mock(modulePath: string, renderMocks: () => Record<string, any>) {
    let original = {
      ...(await import(modulePath))
    }
    let mocks = renderMocks()
    let result = {
      ...original,
      ...mocks
    }
    mock.module(modulePath, () => result)

    this.mocks.push({
      clear: () => {
        mock.module(modulePath, () => original)
      }
    })
  }

  clear() {
    this.mocks.forEach(mockResult => mockResult.clear())
    this.mocks = []
  }
}
MonsterDeveloper commented 3 weeks ago

@jstlaurent the main issue with this approach is that you’re still importing the original module. In many cases mocking is used for the whole module in order to not import it itself or its dependencies.

So right now it’s not possible to restore the module mock in Bun without importing the original module.

Since Bun seems to spawn processes quite fast, I’ve come to love this approach:

find . -name \"*.test.ts\" | xargs -I {} sh -c 'bun test {} || exit 255'

It runs each test file in its own process sequentially and therefore doesn’t come across issues related to module mocking not restoring. It also stops the execution if one of the test files failed.

It’s also possible to increase test run speed by running xargs processes in parallel, yet I found that even sequential runs finish all tests in around 15 seconds (~6k lines of tests).