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

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

Open
yukikwi opened this issue Dec 25, 2023 · 17 comments · May be fixed by #18171
Open

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

yukikwi opened this issue Dec 25, 2023 · 17 comments · May be fixed by #18171
Labels
bug Something isn't working bun:test Something related to the `bun test` runner

Comments

@yukikwi
Copy link

yukikwi commented Dec 25, 2023

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)
  })
})
@yukikwi yukikwi added the bug Something isn't working label Dec 25, 2023
@Electroid Electroid added the bun:test Something related to the `bun test` runner label Dec 25, 2023
@chlorophant
Copy link

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
Copy link

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
Copy link
Collaborator

As a temporary workaround, you can try jest.restoreAllMocks

import {jest} from 'bun:test';

jest.restoreAllMocks();

@jpasquers
Copy link

@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
Copy link

cuquo commented Feb 7, 2024

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
Copy link

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
Copy link

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
Copy link

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

@ziroock
Copy link

ziroock commented Sep 3, 2024

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
Copy link
Contributor

aryzing commented Sep 4, 2024

Seems the conversation has shifted a bit towards cross-test module mocking, here's a related issue on that: #12823

@jstlaurent
Copy link

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
Copy link

@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).

@juriadams
Copy link

Unfortunately, none of the workarounds mentioned above resolved this blocker for me.

While the implementation is pending, perhaps the documentation for Module mocks with mock.module could be updated to include a warning banner indicating that restoring module mocks is currently not possible.

@redbmk
Copy link

redbmk commented Dec 18, 2024

It's really weird, for me mock.restore() seems to be working fine locally, but in CI mocked modules between different test files will conflict with each other. I feel like there must be some environment variable causing different behavior but I'm not able to track it down yet.

I'm using 1.1.39 in both environments

@notsoluckycharm
Copy link

I agree with the conversation, but I wanted to share my way of doing this which doesn't require you calling the .clear method.

import { mock } from "bun:test";

/**
 *
 * @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: string,
  renderMocks: () => Record<string, any>,
) => {
  let original = {
    ...(await import(modulePath)),
  };
  let mocks = renderMocks();
  let result = {
    ...original,
    ...mocks,
  };
  mock.module(modulePath, () => result);
  return {
    [Symbol.dispose]: () => {
      mock.module(modulePath, () => original);
    },
  };
};

This takes advantage of the Typescript using keyword - pretty cool!

    using parseAndRollMock = await mockModule('../commands/utils/parseAndRoll', () => ({
      parseAndRoll: jest.fn().mockReturnValue({ total: 15, details: [15] }),
    }));

No need to call clear or use before/after hooks for this. As soon as it leaves scope, it will call the reset methods defined in Symbol.dispose - credit due to @jpasquers since I borrowed it from there and just expanded on it.

@platzhersh
Copy link

@notsoluckycharm that's a pretty neat fix for now 🙌 How would you make assertions like .toHaveBeenCalledWith with this?

@Chr1d5ter
Copy link

Chr1d5ter commented Feb 11, 2025

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?

The only quick ( and very very dirty ) way I got around this was to...

import * as WeightModule from "./modules/weights";

describe("Weight Module", () => {
  const originalModule = JSON.parse(JSON.stringify(WeightModule));

  beforeAll(() => {
    mock.module("./modules/weights", () => ({
      weightData: weightTestData,
    }));
  });

  afterAll(() => {
    mock.restore();
    mock.module("./modules/weights", () => originalModule);
  });

{Tests go here...}
});

Hope this helps

@Electroid Electroid linked a pull request Mar 13, 2025 that will close this issue
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working bun:test Something related to the `bun test` runner
Projects
None yet
Development

Successfully merging a pull request may close this issue.