如果从另一个模块调用该模块,为什么改变模块会更新引用,但如果从自身调用则不会?

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

此问题与测试 javascript 和模拟功能有关。

假设我有一个看起来像这样的模块:

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

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

那我就无法通过下面的方式测试了:

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'
    });
});

但是,如果将模块一分为二:

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("");
}

然后我可以改变 beta 模块,alpha 知道它:

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

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

为什么会这样?我正在寻找技术上特定的答案。

我有一个 Github 分支,代码为 here,请参阅 mutateModulesingleFunctionPerModuleAndMutate 文件夹。

作为一个附加问题 - 在这个例子中,我通过直接重新分配属性来改变模块。我是否正确理解使用 jest mock 功能本质上会做同样的事情?

即。如果第一个示例不起作用而第二个示例不起作用的原因是由于突变,那么这必然意味着使用 jest 模块模拟函数同样不起作用。

据我所知 - 在测试该模块时,无法模拟模块中的单个函数,as this jest github issues talks about。我想知道的是 - 这是为什么。

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".

当您导入 ES6 模块时,您实际上可以实时查看该模块导出的内容。

实时视图可以改变,任何导入模块导出的实时视图的代码都会看到改变。

这就是当 alphabeta 位于两个不同模块时您的测试有效的原因。测试修改了beta模块的live view,由于alpha模块使用了beta模块的live view,所以自动使用mocked函数代替原来的。

另一方面,在上面的代码中,alphabeta在同一个模块中,alpha直接调用betaalpha使用模块的实时视图,所以当测试修改模块的实时视图时,它没有效果。


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?

有几种方法可以使用 Jest 来模拟事物。

其中一种方法是使用 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

使用jest.spyOn的一种常见方法是将 ES6 模块的实时视图作为改变模块实时视图的对象传递给它。

所以是的,通过将 ES6 模块的实时视图传递给 jest.spyOn(或 Sinon 中的 spyOn from Jasmine, or sinon.spy 等)来模拟会改变模块的实时视图与在上面的代码中直接改变模块的实时视图的方式基本相同。


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.

实际上,可能的。

"ES6 modules support cyclic dependencies automatically"这意味着模块的实时视图可以导入到模块本身.

只要alpha使用定义了beta的模块的实时视图调用beta,那么beta就可以在测试期间被模拟。即使它们是在同一个模块中定义的,这仍然有效:

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("");
}

我觉得有趣的是 none 您的代码可以在浏览器中运行。

模块 (./some/path/to/file.js"):

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

您不能修改命名导入,因为它们是常量:

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

您也不能分配给名称空间导入的只读 属性。

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

这是一个代码笔,它也说明了为什么 indexModule.alpha !== alpha 来自模块:https://codepen.io/bluewater86/pen/QYwMPa


您正在使用该模块来封装您的两个功能,但由于上述原因,这是一个坏主意。您确实需要将这些函数封装在 class 中,以便您可以适当地模拟它们。

//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 }

最后,通过移动到 default/named 导入而不是命名空间导入,您将无需使用循环依赖 hack。使用 default/named 导入意味着您将导入与模块导出的导出相同的内存中视图。即 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");
    });
});