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

本小妞迷上赌 提交于 2019-12-20 03:48:15

问题


This question pertains to testing javascript and mocking functions.

Say I have a module that looks like this:

export function alpha(n) {
    return `${n}${beta(n)}${n}`;
}

export function beta(n) {
    return new Array(n).fill(0).map(() => ".").join("");
}

Then I can't test it the following way:

import * as indexModule from "./index";

//Not what we want to do, because we want to mock the functionality of beta
describe("alpha, large test", () => {
    it("alpha(1) returns '1.1'", () => {
        expect(indexModule.alpha(1)).toEqual("1.1"); //PASS
    });

    it("alpha(3) returns '3...3'", () => {
        expect(indexModule.alpha(3)).toEqual("3...3"); //PASS
    });
});

//Simple atomic test
describe("beta", () => {
    it("beta(3) returns '...'", () => {
        expect(indexModule.beta(3)).toEqual("..."); //FAIL: received: 'x'
    });
});

//Here we are trying to mutate the beta function to mock its functionality
describe("alpha", () => {

    indexModule.beta = (n) => "x";
    it("works", () => {
        expect(indexModule.alpha(3)).toEqual("3x3"); //FAIL, recieved: '3...3'
    });
});

However, if split the module into two:

alpha.js

import { beta } from "./beta";

export function alpha(n) {
    return `${n}${beta(n)}${n}`;
}

beta.js

export function beta(n) {
    return new Array(n).fill(0).map(() => ".").join("");
}

Then I can mutate the beta module, and alpha knows about it:

import { alpha } from "./alpha";
import * as betaModule from "./beta";

describe("alpha", () => {
    betaModule.beta = (n) => "x";
    it("works", () => {
        expect(alpha(3)).toEqual("3x3");   //PASS
    });
});

Why is this the case? I'm looking for a technically specific answer.

I have a Github branch with this code here, see the mutateModule and singleFunctionPerModuleAndMutate folders.

As an additional question - in this example I am mutating the module by directly reassigning properties. Am I right in understanding that using jest mock functionality is going to be essentially doing the same thing?

ie. If the reason that the first example doesn't work but the second doesn't is due to the mutation, then it necceserily means that using the jest module mocking functions is similarly not going to work.

As far as I know - there is not way to mock a single function in a module, while testing that module, as this jest github issues talks about. What I'm wanting to know - is why this is.


回答1:


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

"In ES6, imports are live read-only views on exported values".

When you import an ES6 module you essentially get a live view of what is exported by that module.

The live view can be mutated, and any code that imports a live view of the module exports will see the mutation.

That is why your test works when alpha and beta are in two different modules. The test modifies the live view of the beta module, and since the alpha module uses the live view of the beta module, it automatically uses the mocked function instead of the original.

On the other hand, in the code above alpha and beta are in the same module and alpha calls beta directly. alpha does not use the live view of the module so when the test modifies the live view of the module it has no effect.


As an additional question - in this example I am mutating the module by directly reassigning properties. Am I right in understanding that using jest mock functionality is going to be essentially doing the same thing?

There are a few ways to mock things using Jest.

One of the ways is using jest.spyOn which accepts an object and a method name and replaces the method on the object with a spy that calls the original method.

A common way to use jest.spyOn is to pass it the live view of an ES6 module as the object which mutates the live view of the module.

So yes, mocking by passing the live view of an ES6 module to something like jest.spyOn (or spyOn from Jasmine, or sinon.spy from Sinon, etc.) mutates the live view of the module in essentially the same way as directly mutating the live view of the module like you are doing in the code above.


As far as I know - there is not way to mock a single function in a module, while testing that module, as this jest github issues talks about. What I'm wanting to know - is why this is.

Actually, it is possible.

"ES6 modules support cyclic dependencies automatically" which means that the live view of a module can be imported into the module itself.

As long as alpha calls beta using the live view of the module that beta is defined in, then beta can be mocked during the test. This works even if they are defined in the same module:

import * as indexModule from './index'  // import the live view of the module

export function alpha(n) {
    return `${n}${indexModule.beta(n)}${n}`;  // call beta using the live view of the module
}

export function beta(n) {
    return new Array(n).fill(0).map(() => ".").join("");
}



回答2:


What I find interesting is that none of your code would work in a browser.

Module ("./some/path/to/file.js"):

const x = () => "x"
const y = () => "y"
export { x, y }

You cannot modify a named import as they are constants:

import { x } from "./some/path/to/file.js"
x = () => {} //Assignment to constant variable.

You also cannot assign to a readonly property of a namespace import.

import * as stuff from "./some/path/to/file.js"
stuff.y = () => {} //Cannot assign to read only property 'y' of...

Here's a codepen which also shows why indexModule.alpha !== alpha from the module: https://codepen.io/bluewater86/pen/QYwMPa


You are using the module to encapsulate your two functions but for the reasons above, this is a bad idea. You really need to encapsulate those functions in a class so that you can mock them appropriately.

//alphaBeta.js

export const beta = n => new Array(n).fill(0).map(() => ".").join("");

export default class alphaBeta {
    static get beta() { return beta }
    beta(n) {
        beta(n)
    }
    alpha(n) {
        return `${n}${this.beta(n)}${n}`;
    }
}
export { alphaBeta }

And finally by moving to default/named imports instead of namespace imports you will have no need to use the cyclic dependency hack. Using default/named imports means that you will be importing the same in-memory view of the exports that the module exported. i.e. importer.beta === exporter.beta

import alphaBetaDefault, { alphaBeta, beta } from "./alphaBeta.js"
alphaBeta.prototype.beta = (n) => "x";

describe("alphaBeta", () => {
    it("Imported function === exported function", () => {
        expect(alphaBeta.beta).toEqual(beta); //PASS
    });

    const alphaBetaObject = new alphaBeta
    it("Has been mocked", () => {
        expect(alphaBetaObject.alpha(3)).toEqual("3x3");
    });

    alphaBeta.prototype.beta = (n) => "z";
    it("Is still connected to its prototype", () => {
        expect(alphaBetaObject.alpha(3)).toEqual("3z3");
    });

    const secondObject = new alphaBetaDefault
    it("Will still be mocked for all imports of that module", () => {
        expect(secondObject.alpha(3)).toEqual("3z3");
    });
});


来源:https://stackoverflow.com/questions/54318830/why-does-mutating-a-module-update-the-reference-if-calling-that-module-from-anot

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!