Як знущатися з імпорту модуля ES6?


141

У мене є наступні модулі ES6:

network.js

export function getDataFromServer() {
  return ...
}

widget.js

import { getDataFromServer } from 'network.js';

export class Widget() {
  constructor() {
    getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }

  render() {
    ...
  }
}

Я шукаю спосіб перевірити віджет із макетним екземпляром getDataFromServer. Якби я використовував окремі <script>s замість модулів ES6, як у Кармі, я міг би написати свій тест на зразок:

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Однак якщо я тестую модулі ES6 окремо за межами браузера (як, наприклад, з Mocha + babel), я б написав щось на зразок:

import { Widget } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(?????) // How to mock?
    .andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Гаразд, але зараз getDataFromServerце не доступно window(ну, взагалі немає window), і я не знаю способу ввести речі безпосередньо у widget.jsвласну сферу.

То куди я їхати звідси?

  1. Чи є спосіб отримати доступ до сфери застосування widget.jsабо принаймні замінити його імпорт власним кодом?
  2. Якщо ні, то як я можу зробити Widgetперевірений?

Я розглядав:

а. Ручне введення залежності.

Видаліть увесь імпорт widget.jsі очікуйте, що той, хто телефонує, надасть послуги.

export class Widget() {
  constructor(deps) {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

Мені дуже незручно зіпсувати загальнодоступний інтерфейс віджетів, як це, та викладати деталі щодо імплементації. Не йдіть.


б. Розкрийте імпорт, щоб дозволити знущатися над ними.

Щось на зразок:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

export class Widget() {
  constructor() {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

тоді:

import { Widget, deps } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(deps.getDataFromServer)  // !
      .andReturn("mockData");
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Це менш інвазивно, але вимагає, щоб я написав багато котлів для кожного модуля, і все ж є ризик використання мене getDataFromServerзамість того, щоб deps.getDataFromServerвесь час. Мені це неприємно, але це моя найкраща ідея поки що.


Якщо не існує вбудованої підтримки для такого типу імпорту, я, мабуть, подумав би над тим, щоб написати власний трансформатор для вавільного перетворення вашого імпорту в стилі ES6 у користувальницьку систему імпорту. Це точно додасть ще один шар можливих збоїв і змінює код, який ви хочете перевірити,.
t.niese

Я зараз не можу встановити тестовий набір, але я б спробував використовувати функцію jasmin createSpy( github.com/jasmine/jasmine/blob/… ) із імпортованою посиланням на getDataFromServer з модуля 'network.js'. Отже, у тестовий файл віджета ви імпортуєте getDataFromServer, а потімlet spy = createSpy('getDataFromServer', getDataFromServer)
Microfed

Друга здогадка - повернути об’єкт з модуля 'network.js', а не функцію. Таким чином, ви могли spyOnна тому об'єкті, імпортованому з network.jsмодуля. Це завжди посилання на один і той же об’єкт.
Microfed

Насправді це вже об’єкт, з того, що я бачу: babeljs.io/repl/…
Microfed

2
Я не дуже розумію, як ін'єкційна залежність псує Widgetпублічний інтерфейс? Widgetплутається без deps . Чому б не зробити залежність явною?
thebearingedge

Відповіді:


129

Я почав використовувати import * as objстиль у своїх тестах, який імпортує весь експорт з модуля як властивості об'єкта, з якого потім можна знущатися. Я вважаю, що це набагато чистіше, ніж використання чогось на кшталт rewire або proxyquire або будь-якої подібної техніки. Я робив це найчастіше, коли потрібно знущатися з дій Redux, наприклад. Ось що я можу використати для вашого прикладу вище:

import * as network from 'network.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(network, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Якщо ваша функція буде експортом за замовчуванням, тоді import * as network from './network'буде вироблятися {default: getDataFromServer}і ви можете знущатися з network.default.


3
Чи використовуєте ви import * as objєдине в тесті або також у своєму звичайному коді?
Чау Тай

36
@carpeliam Цей звичай не працює з специфікацією модуля ES6, де імпорт здійснюється лише заново.
ашиш

7
Жасмін скаржиться, [method_name] is not declared writable or has no setterщо має сенс, оскільки імпорт es6 є постійним. Чи є спосіб вирішити?
lpan

2
@Francisc import(на відміну від цього require, який може перейти куди завгодно) піднімається, тому технічно не можна імпортувати кілька разів. Здається, що вашого шпигуна називають деінде? Для того, щоб тести не замішали стан (відомий як тестове забруднення), ви можете скинути своїх шпигунів у AfterEach (наприклад, sinon.sandbox). Жасмін Я вважаю, що це робиться автоматично.
carpeliam

10
@ agent47 Проблема полягає в тому, що, хоча специфікація ES6 спеціально заважає цій відповіді працювати, точно так, як ви згадали, більшість людей, які пишуть importв JS, насправді не використовують модулі ES6. Що - щось на зразок WebPack або Вавилонської втрутиться при побудові часу і перетворити його або в свій власний внутрішній механізм для виклику віддалених частин коду (наприклад __webpack_require__) або в одну з попередньо ES6 де - факто стандартами, CommonJS, AMD або UMD. І це перетворення часто не дотримується строго специфікації. Тож для багатьох, багатьох чортів саме зараз ця відповідь чудово працює. Зараз.
daemonexmachina

31

@carpeliam вірно, але зауважте, що якщо ви хочете шпигувати за функцією в модулі і використовувати іншу функцію в цьому модулі, що викликає цю функцію, вам потрібно викликати цю функцію як частину простору імен експорту, інакше шпигун не буде використовуватися.

Неправильний приклад:

// mymodule.js

export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will still be 2
    });
});

Правильний приклад:

export function myfunc2() {return 2;}
export function myfunc1() {return exports.myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will be 3 which is what you expect
    });
});

4
Я хотів би, щоб я міг проголосувати цю відповідь ще 20 разів! Дякую!
sfletche

Хтось може пояснити, чому це так? Чи Export.myfunc2 () є копією myfunc2 () без прямої посилання?
Колін Вітмарш

2
@ColinWhitmarsh exports.myfunc2- це пряме посилання на те, myfunc2поки не spyOnзамінить його посиланням на функцію шпигуна. spyOnзмінить значення exports.myfunc2та замінить його на об'єкт-шпигун, тоді як він myfunc2залишається недоторканим у межах модуля (оскільки spyOnне має до нього доступу)
madprog

не слід імпортувати *об'єкт із заморожуванням, а атрибути об'єкта не можуть бути змінені?
агент47

1
Лише зауважте, що ця рекомендація щодо використання export functionпоряд із exports.myfunc2технічним змішуванням синтаксису commonjs та ES6 модуля, і це не дозволено в новіших версіях webpack (2+), які вимагають використання синтаксису модуля ES6 або майже нічого. Нижче я додав відповідь на основі цієї, яка буде працювати в жорстких умовах ES6.
QuarkleMotion

6

Я реалізував бібліотеку, яка намагається вирішити проблему знущання з імпорту класу Typescript під час виконання, не вимагаючи від оригінального класу знати про явні введення залежності.

Бібліотека використовує import * asсинтаксис, а потім замінює оригінальний експортований об’єкт класом заглушки. Він зберігає безпеку типу, тому ваші тести будуть порушені під час компіляції, якщо ім'я методу було оновлено без оновлення відповідного тесту.

Цю бібліотеку можна знайти тут: ts-mock-import .


1
Цей модуль потребує більше зірок github
SD

6

@ відповідь vdloo мене направила в правильному напрямку, але використання обох ключових слів "експортування" і модуля ES6 "експортування" разом у одному файлі для мене не працювало (webpack v2 або пізніші скарги). Натомість я використовую експорт за замовчуванням (названа змінна), який обгортає весь окремий експорт модуля, а потім імпортує експорт за замовчуванням у мій тестовий файл. Я використовую наступні налаштування експорту з моккою / синоном і заглушкою, добре працює, не потребуючи перемотування тощо.

// MyModule.js
let MyModule;

export function myfunc2() { return 2; }
export function myfunc1() { return MyModule.myfunc2(); }

export default MyModule = {
  myfunc1,
  myfunc2
}

// tests.js
import MyModule from './MyModule'

describe('MyModule', () => {
  const sandbox = sinon.sandbox.create();
  beforeEach(() => {
    sandbox.stub(MyModule, 'myfunc2').returns(4);
  });
  afterEach(() => {
    sandbox.restore();
  });
  it('myfunc1 is a proxy for myfunc2', () => {
    expect(MyModule.myfunc1()).to.eql(4);
  });
});

Корисна відповідь, дякую. Просто хотів зазначити, що let MyModuleне потрібно використовувати експорт за замовчуванням (це може бути необроблений об'єкт). Крім того, цей метод не вимагає myfunc1()дзвонити myfunc2(), він працює, щоб просто шпигувати за ним безпосередньо.
Марк Едінгтон

@QuarkleMotion: Схоже, ви випадково відредагували цей обліковий запис, ніж ваш головний рахунок. Ось чому ваша редакція повинна була пройти затвердження вручну - це виглядало не так, як це було у вас, я вважаю, що це просто випадковість, але, якщо це було навмисно, вам слід прочитати офіційну політику щодо лялькових акаунтів, щоб ви не випадково порушуйте правила .
Помітний укладач

1
@ConspicuousCompiler дякую за підняту голову - це була помилка, я не збирався змінювати цю відповідь за допомогою мого облікового запису SO, пов’язаного з електронною поштою.
QuarkleMotion

Це, здається, є відповіддю на інше питання! Де widget.js та network.js? Здається, ця відповідь не має перехідної залежності, через що важко було поставити початкове питання.
Bennett McElwee

3

Я виявив, що цей синтаксис працює:

Мій модуль:

// mymod.js
import shortid from 'shortid';

const myfunc = () => shortid();
export default myfunc;

Тестовий код мого модуля:

// mymod.test.js
import myfunc from './mymod';
import shortid from 'shortid';

jest.mock('shortid');

describe('mocks shortid', () => {
  it('works', () => {
    shortid.mockImplementation(() => 1);
    expect(myfunc()).toEqual(1);
  });
});

Дивіться док .


+1 і з деякими додатковими вказівками. Здається, працює лише з модулями вузлів, тобто з речами, які є у пакет.json. І ще важливіше те, що те, що не згадується в документах Jest, рядок, який передається jest.mock(), повинен відповідати імені, яке використовується в import / packge.json замість імені константа. У документах вони обидва однакові, але з кодом, як і import jwt from 'jsonwebtoken'вам, потрібно встановити макет якjest.mock('jsonwebtoken')
kaskelotti

0

Я сам не пробував цього, але думаю, що глузування може спрацювати. Це дозволяє замінити реальний модуль макетом, який ви надали. Нижче наводиться приклад, щоб дати вам уявлення про те, як це працює:

mockery.enable();
var networkMock = {
    getDataFromServer: function () { /* your mock code */ }
};
mockery.registerMock('network.js', networkMock);

import { Widget } from 'widget.js';
// This widget will have imported the `networkMock` instead of the real 'network.js'

mockery.deregisterMock('network.js');
mockery.disable();

Схоже, mockeryце вже не підтримується, і я думаю, що він працює лише з Node.js, але тим більше, це акуратне рішення для глузування з модулів, які інакше важко знущатися.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.