使用Jasmine时ES6导入与CommonJS引入命名导出的可模拟性差异
Great question—this is a super common gotcha when mixing ES6 modules, CommonJS, and testing spies, especially with Babel's module compilation. Let's break down exactly what's happening here, and why it seems to contradict your initial understanding:
1. CommonJS Module Behavior: Mutable Singleton Exports
When you use require('./fileA') in CommonJS:
- Node.js executes the module once, caches its
exportsobject, and returns the same cached object to every subsequentrequirecall. - The
exportsobject is a plain, mutable JavaScript object. When you runspyOn(A, 'foo')in your test file, you're directly modifying thefooproperty on this cachedexportsobject. - Since
commonBaz.jsalsorequire('./fileA')and gets the same cached object, any call toA.foo()in that file will use the spy function you created—hence the test succeeds.
This aligns with the "copy" behavior you mentioned, but the key detail is that it's a copy of a reference to the cached exports object, not a copy of the function itself. So modifying the object's properties affects all consumers.
2. Babel-Compiled ES6 Modules: Read-Only Binding Views
ES6 modules spec defines import * as A as a module namespace object—an immutable object where each property is a live binding to the module's exports. Babel tries to simulate this behavior when compiling ES6 modules to CommonJS, but with a critical catch for testing:
- When you
import * as A from './fileA', Babel compiles this to wrap the CommonJSexportsobject in a structure that enforces read-only properties (to match ES6 spec). spyOn(A, 'foo')attempts to replace thefooproperty on this namespace object, but since the property is read-only (or implemented via getters that point directly to the original function infileA.js), the spy doesn't actually override the realfoofunction.es6Baz.jsuses its ownimport * as Abinding, which still points to the originalfoofunction infileA.js—so the spy in your test file never catches the call.
This seems to contradict your "live binding" intuition, but the issue is that live bindings in ES6 don't let you reassign the binding itself—you can only modify the value if the module exports a mutable value (like an object with methods). Since you're exporting top-level functions, the bindings are read-only.
3. Why Your Initial Intuition Seemed Reversed
You were right that ES6 uses live bindings and CommonJS uses value copies—but in this testing scenario:
- CommonJS's mutable exports object lets you overwrite the function reference globally (since all consumers share the same cached object).
- ES6's read-only module namespace objects prevent you from overwriting the binding, so the original function remains in use across all imports.
Quick Fixes for the ES6 Spy Issue
If you want to spy on ES6 exports with Babel, try these options:
- Export an object containing your functions instead of top-level named exports (so you can spy on the object's mutable methods).
- Use a module mocking tool designed for ES6 modules (like Jest's
jest.mockwith ES6 syntax, orbabel-plugin-rewire). - For test-only builds, tweak Babel config to disable read-only enforcement for module namespace objects (not recommended for production).
内容的提问来源于stack exchange,提问作者Johnny

