如果从另一个模块调用该模块,为什么对一个模块进行突变却不能更新引用呢?

时间:2019-01-23 01:31:54

标签: javascript jestjs es6-modules

此问题与测试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模拟功能本质上会做同样的事情,对吗?

即。如果第一个示例不起作用而第二个示例不起作用的原因是由于该突变,那么它恰好意味着使用jest模块的模拟功能同样行不通。

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

2 个答案:

答案 0 :(得分:2)

  

如果从另一个模块调用该模块,为什么突变一个模块会更新引用,而不是从其自身调用,为什么不更新呢?

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

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

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

这就是为什么当alphabeta在两个不同的模块中时,您的测试有效的原因。该测试修改了beta模块的实时视图,并且由于alpha模块使用了beta模块的实时视图,因此它会自动使用模拟功能而不是原始功能。

另一方面,在alphabeta以上的代码中,它们位于同一模块中,并且 alpha直接调用beta alpha 使用模块的实时视图,因此,当测试修改模块的实时视图时,它将无效。


  

另一个问题-在本示例中,我通过直接重新分配属性来对模块进行变异。我理解使用Jest模拟功能本质上会做同样的事情吗?

有几种使用Jest模拟事物的方法。

其中一种方法是使用jest.spyOn接受一个对象和一个方法名称以及replaces the method on the object with a spy that calls the original method

使用jest.spyOn的一种常见方法是将ES6模块的实时视图作为对象传递给它,从而使该模块的实时视图发生变化。

是的,可以通过将ES6模块的实时视图传递到jest.spyOn(或Jasmine的{​​{3}}或Sinon的{​​{3}}这样的模拟对象中来等),就像在上面的代码中所做的那样,与直接更改模块的实时视图的方式基本上相同。


  

据我所知-在测试模块时,无法模拟模块中的单个功能,正如github问题所谈论的那样。我想知道的-这就是为什么。

实际上,这是有可能的。

spyOn,这意味着模块的实时视图可以导入到模块本身中

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

答案 1 :(得分:1)

我发现有趣的是,您的代码都无法在浏览器中工作。

模块(“ ./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


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

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

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