Compare commits

...

2 Commits

Author SHA1 Message Date
Electroid
d20eaf3e36 bun run clang-format 2025-03-13 22:05:17 +00:00
Ashcon Partovi
f02078f413 Fix mock.restore() not working with mock.module() 2025-03-13 15:00:02 -07:00
6 changed files with 143 additions and 1 deletions

70
docs/api/mock/module.md Normal file
View File

@@ -0,0 +1,70 @@
# `mock.module()`
The `mock.module()` function allows you to mock an entire module in Bun's test framework. This is useful when you want to replace a module's exports with mock implementations.
## Example
```typescript
import { test, expect, mock } from "bun:test";
import { foo } from "./some-module";
test("mock.module works", () => {
// Original behavior
expect(foo()).toBe("original");
// Mock the module
mock.module("./some-module", () => ({
foo: () => "mocked"
}));
// Mocked behavior
expect(foo()).toBe("mocked");
});
```
## Restoring mocked modules
When you use `mock.restore()` to restore a mocked module, it clears the mocked implementation but the imported module might still reference the mocked version. To fully restore the original module, you need to re-import it:
```typescript
import { test, expect, mock } from "bun:test";
import { foo } from "./some-module";
test("mock.restore works with mock.module", async () => {
// Original behavior
expect(foo()).toBe("original");
// Mock the module
mock.module("./some-module", () => ({
foo: () => "mocked"
}));
// Mocked behavior
expect(foo()).toBe("mocked");
// Restore all mocks
mock.restore();
// Re-import the module to get the original behavior
const module = await import("./some-module?timestamp=" + Date.now());
const restoredFoo = module.foo;
// Original behavior is restored
expect(restoredFoo()).toBe("original");
});
```
The query parameter (`?timestamp=...`) is added to bypass the module cache, forcing a fresh import of the original module.
## API
### `mock.module(specifier: string, factory: () => Record<string, any>): void`
- `specifier`: The module specifier to mock. This can be a relative path, package name, or absolute path.
- `factory`: A function that returns an object with the mock exports. This object will replace the real exports of the module.
## Notes
- Mocked modules affect all imports of the module, even imports that occurred before the mock was set up.
- Use `mock.restore()` to clear all mocks, including mocked modules.
- You need to re-import the module after `mock.restore()` to get the original behavior.

View File

@@ -386,6 +386,15 @@ void BunPlugin::OnLoad::addModuleMock(JSC::VM& vm, const String& path, JSC::JSOb
virtualModules->set(path, JSC::Strong<JSC::JSObject> { vm, mockObject });
}
void BunPlugin::OnLoad::clearModuleMocks()
{
if (virtualModules) {
// Clear the virtual modules map
// When code tries to import the module again, the original will be loaded
virtualModules->clear();
}
}
class JSModuleMock final : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;

View File

@@ -76,6 +76,7 @@ public:
bool hasVirtualModules() const { return virtualModules != nullptr; }
void addModuleMock(JSC::VM& vm, const String& path, JSC::JSObject* mock);
void clearModuleMocks();
std::optional<String> resolveVirtualModule(const String& path, const String& from);

View File

@@ -1063,8 +1063,25 @@ JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockRestore, (JSC::JSGlobalObject * globa
auto scope = DECLARE_THROW_SCOPE(vm);
CHECK_IS_MOCK_FUNCTION(thisValue);
// First clear any function spies
thisObject->clearSpy();
// Then reset module mocks
// Get the GlobalObject as Zig::GlobalObject for access to our module mockery
if (auto* zigGlobalObject = jsDynamicCast<Zig::GlobalObject*>(globalObject)) {
// Clear the virtual modules map - removes module mocks
zigGlobalObject->onLoadPlugins.clearModuleMocks();
// Call the reload method which will:
// 1. Clear the ESM registry
// 2. Clear the CommonJS require cache
// 3. Run GC to clean up old references
zigGlobalObject->reload();
// Reset the internal reload count to ensure GC always runs on next reload
// which helps modules get reloaded properly
zigGlobalObject->reloadCount = 0;
}
RELEASE_AND_RETURN(scope, JSValue::encode(thisObject));
}
JSC_DEFINE_HOST_FUNCTION(jsMockFunctionMockImplementation, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callframe))
@@ -1413,7 +1430,24 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSetSystemTime, (JSC::JSGlobalObject * globalO
BUN_DEFINE_HOST_FUNCTION(JSMock__jsRestoreAllMocks, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callframe))
{
JSMock__resetSpies(jsCast<Zig::GlobalObject*>(globalObject));
if (auto* zigGlobalObject = jsDynamicCast<Zig::GlobalObject*>(globalObject)) {
// Reset all spies
JSMock__resetSpies(zigGlobalObject);
// Clear module mocks
zigGlobalObject->onLoadPlugins.clearModuleMocks();
// Call the reload method which will:
// 1. Clear the ESM registry
// 2. Clear the CommonJS require cache
// 3. Run GC to clean up old references
zigGlobalObject->reload();
// Reset the internal reload count to ensure GC always runs on next reload
// which helps modules get reloaded properly
zigGlobalObject->reloadCount = 0;
}
return JSValue::encode(jsUndefined());
}

View File

@@ -0,0 +1,3 @@
export function foo() {
return "foo";
}

View File

@@ -0,0 +1,25 @@
import { test, expect, mock } from "bun:test";
import { foo } from "./07823.fixture";
test("mock.restore() works with mock.module()", async () => {
// First, verify original behavior
expect(foo()).toBe("foo");
// Mock the module
mock.module("./07823.fixture", () => ({
foo: () => "bar",
}));
// Verify the mock works
expect(foo()).toBe("bar");
// Restore the mock
mock.restore();
// Re-import the module to get the original behavior
const module = await import("./07823.fixture?timestamp=" + Date.now());
const restoredFoo = module.foo;
// Verify original behavior is restored
expect(restoredFoo()).toBe("foo");
});