Почему мутирование модуля обновляет ссылку при вызове этого модуля из другого модуля, но не при вызове от самого себя? - PullRequest
0 голосов
/ 23 января 2019

Этот вопрос касается тестирования функций javascript и mocking.

Скажем, у меня есть модуль, который выглядит следующим образом:

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

Тогда я могу мутироватьбета-модуль, и альфа знает об этом:

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

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

Почему это так?Я ищу технически конкретный ответ.

У меня есть ветка Github с этим кодом здесь , см. Папки mutateModule и singleFunctionPerModuleAndMutate.

В качестве дополнительного вопроса - в этом примере я изменяю модуль, напрямую переназначая свойства.Правильно ли я понимаю, что использование шутливой функциональности будет по существу делать то же самое?

т.е.Если причина того, что первый пример не работает, а второй нет, связана с мутацией, то это неизбежно означает, что использование насмешливых функций модуля jest аналогично не будет работать.

Насколько я знаю, во время тестирования этого модуля нет способа смоделировать одну функцию в модуле, , так как этот jest github говорит о .То, что я хочу знать, - вот почему.

Ответы [ 2 ]

0 голосов
/ 23 января 2019

Что мне кажется интересным, так это то, что ни один из ваших кодов не будет работать в браузере.

Модуль ("./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");
    });
});
0 голосов
/ 23 января 2019

Почему изменение модуля обновляет ссылку при вызове этого модуля из другого модуля, но не при вызове из самого себя?

"В ES6 импорт - это оперативные представления только для чтения для экспортируемых значений" .

Когда вы импортируете модуль ES6, вы по существу получаете представление в реальном времени о том, что экспортируется этим модулем.

Оперативное представление может быть видоизменено, и любой код, который импортирует динамическое представление экспорта модуля, увидит мутацию.

Вот почему ваш тест работает, когда alpha и beta находятся в двух разных модулях. Тест изменяет динамическое представление модуля beta, и поскольку модуль alpha использует динамическое отображение модуля beta, он автоматически использует фиктивную функцию вместо оригинальной.

С другой стороны, в приведенном выше коде alpha и beta находятся в одном модуле, а alpha вызывает beta напрямую . alpha не не использует просмотр в реальном времени модуля, поэтому, когда тест изменяет просмотр в реальном времени модуля, он не действует.


В качестве дополнительного вопроса - в этом примере я изменяю модуль, напрямую переназначая свойства. Правильно ли я понимаю, что использование шутливой функциональности будет по существу делать то же самое?

Есть несколько способов издеваться над вещами, используя Jest.

Одним из способов является использование jest.spyOn, которое принимает объект и имя метода, а заменяет метод объекта шпионом, который вызывает оригинальный метод .

Распространенный способ использования jest.spyOn состоит в том, чтобы передать его в режиме реального времени модуля ES6 в качестве объекта, который изменяет динамическое представление модуля.

Так что да, насмешливо, передавая в режиме реального времени модуль ES6 чему-то вроде jest.spyOn (или spyOn из Jasmine или sinon.spy из Sinon и т. Д.) Изменяет динамическое представление модуля по существу так же, как и непосредственное изменение динамического просмотра модуля, как вы делаете в приведенном выше коде.


Насколько я знаю, во время тестирования этого модуля нет способа макетировать одну функцию в модуле, о чем говорит этот шутливый выпуск github. То, что я хочу знать, - вот почему.

На самом деле, возможно .

«Модули ES6 поддерживают циклические зависимости автоматически» , что означает, что динамическое представление модуля можно импортировать в сам модуль .

Пока 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("");
}
...