【问题标题】:Why does mutating a module update the reference if calling that module from another module, but not if calling from itself?如果从另一个模块调用该模块,但如果从其自身调用,为什么变异模块会更新引用?
【发布时间】:2019-01-23 01:31:54
【问题描述】:

这个问题与测试 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
    });
});

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

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

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

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

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

【问题讨论】:

  • 这根本不是 Jest Mocks 的工作方式。文档非常清楚自动模拟和手动模拟的工作原理。
  • @RandyCasburn 我在问一个问题,为什么在一种情况下保留对函数的引用,而不是在另一种情况下,作为了解使用 jest 模拟时它是如何工作的起点。
  • 将您的导入从 * 更改为 { alpha, beta }

标签: javascript jestjs es6-modules


【解决方案1】:

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

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

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

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

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

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


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

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

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

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

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


据我所知 - 没有办法在模块中模拟单个函数,同时测试该模块,正如这个 github 上的笑话所讨论的那样。我想知道 - 为什么会这样。

其实,这可能的。

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

【讨论】:

  • 特别是关于将模块导入自身的那一点 - 这很整洁!
  • @dwjohnston 是的,导入自身是一种很酷的方法。看看 jest.spyOn 在原生支持 ES6 模块后会发生什么将会很有趣,因为我们知道它目前是 the babel transpilation that makes it work
【解决方案2】:

我觉得有趣的是,您的任何代码都不能在浏览器中运行。

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

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

【讨论】:

  • 有趣的答案 - 谢谢。有什么需要考虑的。请注意,这些测试是开玩笑的测试——所以它们不会在浏览器中运行。所以也许 babel-jest 正在做一些允许突变的事情。我会调查的。
  • 我觉得 node 和 webpack 的现状正在冲淡 ES6 引入的严格性。自去年 9 月以来,我们通过直接在浏览器中测试非转译代码来颠覆我们的单元测试。只是思考的食物。
猜你喜欢
  • 2011-04-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多