Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does jest allow mutation of modules?

In this question that I asked here:

Why does mutating a module update the reference if calling that module from another module, but not if calling from itself?

I'm asking about the nature of module mutation.

However as it it turns out, ES6 modules can't actually be mutated - all of their properties are treated as constants. (See this answer)

But somehow - when Jest tests modules - they can be mutated, and that's how Jest allows for mocking.

How is this happening?

I imagine that it's a babel plugin that that's running - transpiling the module to CommonJS modules? Is there any documentation about this?

Is there a way to view the transpiled code?

like image 726
dwjohnston Avatar asked Jan 25 '19 02:01

dwjohnston


1 Answers

ES6 modules can't actually be mutated - all of their properties are treated as constants.

Interesting. You're right, even something as simple as this:

import * as lib from "./lib";  // import an ES6 module
const spy = jest.spyOn(lib, 'someFunc');  // spy on someFunc

...technically shouldn't be allowed since jest.spyOn replaces the method on the object with a spy and lib.someFunc should be a binding to someFunc in the ES6 module.


But somehow - when Jest tests modules - they can be mutated, and that's how Jest allows for mocking.

How is this happening?

They can only be mutated because Jest isn't actually using ES6 modules.

(I guess for completeness it might be possible to run Jest using actual ES6 modules by using Node's experimental support for ES6 Modules but I haven't tried).


I imagine that it's a babel plugin that that's running - transpiling the module...Is there any documentation about this?

"babel-jest is automatically installed when installing Jest and will automatically transform files if a babel configuration exists in your project. To avoid this behavior, you can explicitly reset the transform configuration option".

So by default Jest will use babel-jest which transpiles the source code using babel (and does a few other things like hoisting calls to jest.mock).

Note that Jest can be also be configured using transform which maps "regular expressions to paths to transformers".


Is there a way to view the transpiled code?

Yes. Transformations are done in jest-runtime here and the output is saved to a cache here.

The easiest way to look at the transpiled code is to view the cache.

You can do that by running Jest with the --showConfig option which will output the config used when running Jest. The cache location can be found by looking at the value of "cacheDirectory".

Then run Jest with the --clearCache option to clear out the cache.

Finally, run Jest normally and the cache directory will contain the transpiled code for your project.


Example

The latest Jest (v24) will transpile this code:

// lib.js
export const someFunc = () => 1;


// code.js
import { someFunc } from './lib';
export const func = () => someFunc() + 1;


// code.test.js
import { func } from './code';
import * as lib from './lib';

test('func', () => {
  const spy = jest.spyOn(lib, 'someFunc');
  func();
  expect(spy).toHaveBeenCalled();  // SUCCESS
});

...to this:

// lib.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.someFunc = void 0;
const someFunc = () => 1;
exports.someFunc = someFunc;


// code.js
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.func = void 0;
var _lib = require("./lib");
const func = () => (0, _lib.someFunc)() + 1;
exports.func = func;


// code.test.js
"use strict";
var _code = require("./code");
var lib = _interopRequireWildcard(require("./lib"));
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }
test('func', () => {
  const spy = jest.spyOn(lib, 'someFunc');
  (0, _code.func)();
  expect(spy).toHaveBeenCalled(); // SUCCESS
});

The import * as lib from 'lib'; line gets handled by _interopRequireWildcard which uses require under the hood.

Every call to require "will get exactly the same object returned, if it would resolve to the same file" so code.js and code.test.js are getting the same object from require('./lib').

someFunc is exported as exports.someFunc which allows it to be reassigned.


So yes, you're exactly right. Spying (or mocking) like this only works because the ES6 modules are getting transpiled by babel into Node modules in a way that allows them to be mutated.

like image 126
Brian Adams Avatar answered Sep 22 '22 23:09

Brian Adams