Sinon: Ошибки с .stub () и .spy (), вызванные геттерами / сеттерами

Созданный на 5 апр. 2018  ·  31Комментарии  ·  Источник: sinonjs/sinon

Чего вы ожидали?

В @thumbtack мы находимся в процессе обновления нашей сборки до Webpack 4. В рамках этого некоторые из наших модульных тестов начали давать сбой. Мы отследили это до тех случаев, когда мы использовали функции Sinon .spy и .stub для модулей, которые экспортируются с использованием нестандартного экспорта ES6 в форме export function foo . Похоже, что под капотом Webpack создает геттеры и сеттеры для этого экспорта для своей новой реализации модулей Harmony. Это изменилось с версии 3 на версию 4.

Мы также смогли воспроизвести это независимо от Webpack, попытавшись заглушить простой объект, у которого есть геттер или сеттер.

Что на самом деле происходит

При использовании .stub заглушка работает, но более поздний вызов .restore не работает, и утверждение не выполняется.

При использовании .spy выдается следующая ошибка: TypeError: Attempted to wrap undefined property foo as function . По какой-то причине Синон считает свойство неопределенным, когда оно также существует как геттер.

Как воспроизвести

Я создал репо с минимальным воспроизведением проблем stub и spy с последними версиями Webpack и Sinon. Он также имеет базовый случай, который показывает, что эта проблема не возникает в объектах, которые не импортируются таким образом и, следовательно, не используют сеттеры.

Вы можете клонировать репо здесь: https://github.com/lavelle/sinon-stub-error

Запустите yarn install а затем запустите

  • yarn pass чтобы увидеть базовый вариант
  • yarn fail чтобы увидеть случай неисправной заглушки
  • yarn spy чтобы увидеть провалившееся дело о шпионаже

Заранее спасибо! Мы будем рады отправить PR для решения этой проблемы, если вы укажете нам правильное направление.

cc @bawjensen @dcapo

Property accessors

Самый полезный комментарий

@mroderick

OK. Давайте сделаем это шаг за шагом.

Экспорт ESM использует неизменяемую привязку .

Итак, когда вы пишете:

export const x = value;

Фактически сгенерированный код в конечном итоге вызовет:

Object.defineProperty(exports, name, {
    configurable: ?, // whether this is true or false depends on the bundler at the moment.
    enumerable: true,
    get: getter
});

где name - это x а getter - функция, возвращающая value .

Когда вы пишете:

import { x } from './X';

Импортируется геттер для x , а не значение. Кроме того, поскольку в дескрипторе свойства нет set , этот импорт доступен только для чтения (неизменяемый).

Вот почему Sinon.Stub(object, "method") не работает (должно вызывать TypeError в строгом режиме).

Однако до тех пор, пока сборщик устанавливает configurable: true , экспорт по-прежнему доступен только для чтения, но это можно изменить примерно так:

    Object.defineProperty(logger, 'createLogger', {
      writable: true,
      value: sinon.stub().returns(loggerStub)
    });

Это то, что нужно сделать sinon для поддержки импорта es6-заглушек.


Я не уверен на 100%, о чем именно идет речь - комментарии, похоже, расходятся с исходной проблемой. Но в основном, если этот тикет сообщает о невозможности заглушить импорт es6, то это решение.

Все 31 Комментарий

Похоже, это может быть связано с https://github.com/sinonjs/sinon/issues/1741 , но это не указывает на то, что возникла ошибка. Нам также в идеале не нужно использовать специальные методы Sinon для имитации геттеров, и мы могли бы использовать существующие методы .spy и .stub . Я полагаю, что эта проблема станет более распространенной по мере того, как все больше проектов будут обновлены для использования Webpack 4.

Вот пример того, что Webpack компилирует код под капотом.

Пример файла ES6 с API, например

export function get(url, data, options) {

}

export function post(url, data, options) {

}

export function getJSON(url, data, options) {

}

Производит этот код в Webpack 3

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony export (immutable) */ __webpack_exports__["get"] = get;
/* harmony export (immutable) */ __webpack_exports__["post"] = post;
/* harmony export (immutable) */ __webpack_exports__["getJSON"] = getJSON;
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_jquery__ = __webpack_require__(13);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_jquery___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_jquery__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_lodash__ = __webpack_require__(1);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_lodash___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_lodash__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2__globals_scripts_csrf_es6__ = __webpack_require__(40);
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

И этот код в Webpack 4:

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, {
            configurable: false,
            enumerable: true,
            get: getter
        });
    }
};

// later

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "get", function() { return get; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "post", function() { return post; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getJSON", function() { return getJSON; });
/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! jquery */ "jquery");
/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! lodash */ "./node_modules/lodash/lodash.js");
/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(lodash__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _globals_scripts_csrf_es6__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../globals/scripts/csrf.es6 */ "./globals/scripts/csrf.es6.js");
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

Как вы можете видеть, реализация модуля теперь использует Object.defineProperty для динамического создания таким образом функций получения для экспортируемых функций. Я не уверен, почему это изменилось, но, по-видимому, это связано с поддержкой некоторых новых функций модульной системы. Я не думаю, что это ошибка в Webpack, потому что код отлично работает во всех браузерах и в Node. Проблема возникает только при использовании с Sinon.

Я нашел раздел в документации, в котором говорится

API-заглушка
Если вам нужно заглушить геттеры / сеттеры или нефункциональные свойства, вам следует использовать sandbox.stub

Однако я переключился на sandbox.stub и получил ту же ошибку.

Единственное, что исправляет для меня, это замена

import * as obj from './index';

с участием

import { foo } from './index';
const obj = { foo };

поскольку это не позволяет Webpack создавать геттеры в скомпилированном коде.

Я бы предпочел не делать этого на протяжении всей нашей кодовой базы, import * должен быть допустимым способом импорта. Я понимаю, что это отчасти проблема Webpack, но, учитывая, насколько широко он используется в сообществе, было бы неплохо исправить это в Sinon, чтобы эти два инструмента были совместимы.

Также в идеале нам не нужно было бы использовать специальные методы Sinon для имитации геттеров, и мы могли бы использовать существующие методы .spy и .stub. Я полагаю, что эта проблема станет более распространенной по мере того, как все больше проектов будут обновлены для использования Webpack 4.

Мы тоже, но эта проблема еще не решена.

Заглушки геттеров / сеттеров добавлены в sinon@2 из # 1297

Следующая версия Sinon npm i sinon<strong i="5">@next</strong> --save-dev использует песочницу по умолчанию на sinon , что означает, что вам не нужно явно использовать sandbox .

Однако он по-прежнему использует stub.get() и stub.set() для геттеров / сеттеров.

Пинг @lucasfcosta : у вас есть идеи по этому поводу?

Как упоминалось в @lavelle . При импорте esm в Webpack 4 используются неизменяемые привязки со следующей конфигурацией:

Object.defineProperty(exports, name, {
    configurable: false,
    enumerable: true,
    get: getter
});

Имея ту же проблему, я написал крошечный плагин webpack, чтобы переопределить configurable: false на configurable: true , поэтому мы можем использовать sinon для заглушки следующим образом:

    Object.defineProperty(logger, 'createLogger', {
      writable: true,
      value: sinon.stub().returns(loggerStub)
    });

В той же проблеме (см. Последний раздел) я утверждал, что Webpack следует рассмотреть возможность наличия configurable: true а не configurable: false (так что плагин не нужен). Не знаю, возьмут они это или нет.

Но, в любом случае, может быть разумно рассмотреть возможность вызова Sinon.Stub(object, "method") Object.getOwnPropertyDescriptor и если он имеет отличительную черту импорта esm, вставьте заглушку (так что нам не нужно писать код выше ).

@Izhaki Я не уверен, что понимаю, что вы предлагаете в качестве решения.

Не могли бы вы создать исполняемый пример, демонстрирующий вашу идею способом, который не является специфическим для каких-либо загрузчиков модулей (Webpack, Rollup), но представляет собой чистый JS и может использоваться непосредственно в поддерживающих средах выполнения ESM, как в вечнозеленых браузерах?

@mroderick

OK. Давайте сделаем это шаг за шагом.

Экспорт ESM использует неизменяемую привязку .

Итак, когда вы пишете:

export const x = value;

Фактически сгенерированный код в конечном итоге вызовет:

Object.defineProperty(exports, name, {
    configurable: ?, // whether this is true or false depends on the bundler at the moment.
    enumerable: true,
    get: getter
});

где name - это x а getter - функция, возвращающая value .

Когда вы пишете:

import { x } from './X';

Импортируется геттер для x , а не значение. Кроме того, поскольку в дескрипторе свойства нет set , этот импорт доступен только для чтения (неизменяемый).

Вот почему Sinon.Stub(object, "method") не работает (должно вызывать TypeError в строгом режиме).

Однако до тех пор, пока сборщик устанавливает configurable: true , экспорт по-прежнему доступен только для чтения, но это можно изменить примерно так:

    Object.defineProperty(logger, 'createLogger', {
      writable: true,
      value: sinon.stub().returns(loggerStub)
    });

Это то, что нужно сделать sinon для поддержки импорта es6-заглушек.


Я не уверен на 100%, о чем именно идет речь - комментарии, похоже, расходятся с исходной проблемой. Но в основном, если этот тикет сообщает о невозможности заглушить импорт es6, то это решение.

Вот более конкретный пример того, как мы включили заглушку импорта es6:

const isEs6Import = (object, property) => {
    const descriptor = Object.getOwnPropertyDescriptor(object, property) || {};
    // An es6 import will have get && enumerable.
    // Non-es6 imports should have writable && value.
    return descriptor.get && descriptor.enumerable;
  };

  // Takes a property that is not writable (such as an es6 import) and makes
  // it writable.
  // Should throw in strict mode if the property doesn't have configurable: true.
  const makePropertyWritable = (object, property) => Object.defineProperty(
    object,
    property,
    {
      writable: true,
      value: object[property]
    }
  );

  /** Create a new sandbox before each test **/
  helper.sinon = sinon.sandbox.create();

  const sinonStub = helper.sinon.stub.bind(helper.sinon);
  helper.sinon.stub = function (object, property) {
    if (object && isEs6Import(object, property)) {
      // Es6 imports are read-only by default.
      // So make them writable so we can mock them.
      makePropertyWritable(object, property);
    }
    return sinonStub(object, property);
  };

Вы правы, это повсюду. Все, что связано с получением / установкой / песочницей, отвлекает от основной мысли. На многих этапах есть путаница, но я думаю, вы подвели итог, @lzhaki.

  1. Это не ошибка Sinon.
  2. Документы о том, как создаются геттеры / сеттеры, не были прочитаны, поэтому жалобы на их работу больше связаны с неправильным использованием (также: он потерпит неудачу, если configurable был false, как упоминалось)
  3. На первый взгляд это запрос функции, чтобы упростить тестирование нового кода, транслируемого из Webpack. Настройка Sinon на форматы инструментов и причуд не так уж и интересна с моей точки зрения, и что-то, что обычно следует обрабатывать на этапе сборки (за пределами Sinon), например, то, что было сделано с использованием вашего плагина, чтобы сделать аксессоры настраиваемыми.

Я симпатизирую идее сделать Sinon более «автоматическим», но я не уверен, что предлагаемое исправление является достаточным или устойчивым к ошибкам. Что составляет «отличительные черты модуля ESM»? Как мы можем надежно определить, что имеем дело с транспилированным ESM, а не с каким-то общим объектом, у которого есть геттер? У нас уже есть явная поддержка переопределения аксессоров, так что это не сработает.

Конечно, мы могли бы добавить дополнительные методы, называемые sinon.stubImport или что-то в этом роде, но это метод очень ограниченного использования и временного диапазона.

Имейте в виду, что это будет работать / иметь смысл только в мире транспиляции на ES5, поскольку модули ES действительно неизменяемы. Мы явно это обнаруживаем и говорим, что не можем это поддерживать, поэтому https://github.com/sinonjs/sinon/blob/master/test/es2015/module-support-assessment-test.mjs.

Когда люди заканчивают загрузку собственных ESM и HTT2, что делает бандлинг устаревшим, все эти хаки исчезают.

Я думаю, что простое добавление возможностей шпионажа по умолчанию к существующим заглушкам доступа - лучшее решение, которое также можно было бы использовать в более широком смысле. См. # 1741 для обсуждения этого.


Я нахожусь в отпуске без компьютера, поэтому не могу протестировать код, но я предполагаю, что шаги, необходимые оригинальному плакату для достижения его намеченной цели, сводятся к следующему:

  1. Используйте упомянутый плагин Webpack, чтобы настроить переносимый экспорт.
  2. Заглушите экспорт следующим образом:
    sinon.stub(myModule, 'foo').get( ()=>42 )

Как обсуждалось в # 1741, переданная заглушка в настоящее время обеспечивает только поведение, а не слежку. Пока кто-то (@RoystonS?) Не расширит API, вам нужно будет передать шпионскую функцию в качестве получателя для проверки взаимодействий. Лучше бы документы были хороши, согласен ...

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

Ссылки:

@ fatso83

Хотя то, что вы говорите, имеет большой смысл, позвольте мне возразить.

Что случилось?

Мы хотели получить выгоду от встряхивания дерева, поэтому мы переключаем Typescript на выдачу модулей es6, а не es5 - тогда все наши тесты sinon.stub терпят неудачу.

Это только начало

Я был бы очень удивлен, если esm не возобладает в ближайшем будущем. В этом слишком много хорошего по сравнению с другими парадигмами.

Вероятно, у вас будет все больше и больше людей, которые обращаются к этой проблеме или открывают новые выпуски по этому поводу.

Это не про веб-пакет

Реализация импорта esm в качестве получателя, похоже, не является выбором webpack - похоже, что это способ соответствовать стандарту.

Поэтому в следующем примере замените «webpack» и «формат инструментария» на «стандартный»:

На первый взгляд это запрос функции, чтобы упростить тестирование нового кода, транслируемого из Webpack. С моей точки зрения, настройка Sinon на форматы инструментов и причуды не так уж и интересна ...

Это никогда не было предложенным решением

Я симпатизирую идее сделать Sinon более «автоматическим», но я не уверен, что предлагаемое исправление является достаточным или устойчивым к ошибкам.

Код, который я предоставил, был всего лишь базовым примером - далек от «решения» для sinon.

Следите за своей поверхностью API

Как мы можем надежно определить, что имеем дело с транспилированным ESM, а не с каким-то общим объектом, у которого есть геттер? У нас уже есть явная поддержка переопределения аксессоров, так что это не сработает.

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

Почему я должен использовать .return в одном случае и .get в другом? Для меня как для тестировщика это деталь реализации.


Как бы то ни было, я надеюсь, что я ошибаюсь во всем этом, и это действительно проблема очень немногих людей, которым, как мне кажется, кажется, что это так. Нам просто нужно подождать и посмотреть.

Реализация импорта esm в качестве получателя, похоже, не является выбором webpack - похоже, что это способ соответствовать стандарту.

Да, это способ соблюдения стандарта в средах, которые не поддерживают / не реализуют модули ES, и где вам нужен эффект модульной системы в среде, которая изначально не поддерживает ее. Транспилированный код, вероятно, будет фактическим стандартом того, как модули ES будут использоваться в течение некоторого времени, пока поддержка не станет повсеместной. Но это не настоящая вещь.

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

Явный таргетинг на выходной формат Webpack не кажется хорошим способом тратить ресурсы на обслуживание, поскольку это движущаяся цель, которая, возможно, не понадобится через пару лет, но цель упрощения использования API - хорошая. . Как вы сказали, почему это должно вас волновать. Прямо сейчас я не совсем уверен, были ли / какие у нас были технические причины для создания API, который мы сделали, но ATM я действительно не понимаю, почему мы не можем избавиться от ненужной заглушки, используемой сегодня в заглушке аксессуаров. Я все же упомяну некоторые недостатки этого упрощения ниже, возможно, поэтому первоначальный автор функциональности Sinon 2+ принял решения API, которые он сделал.

Дескрипторы свойств сохраняют надмножество значения свойства, верно? Таким образом, его можно использовать везде, где мы сегодня сохраняем значения (например, исходную функцию). Всегда сохраняя исходный дескриптор свойства и используя дескрипторы свойств при назначении новых заглушек, мы должны иметь возможность повторно использовать ту же логику (хотя я подозреваю, что изменения в кодовой базе Sinon могут быть довольно агрессивными ...). Это уберет явную поддержку другой логики установщика и получателя, но это снова можно решить в заглушке, проверив, использовался ли он как сеттер или получатель.

Это сделало бы API более приятным в некоторых случаях (хотя заглушки аксессуаров сегодня относительно редки), но также могло бы запутать другие случаи: особенно заглушки связанных модулей Webpack. Чтобы убедиться в этом, рассмотрим, какую информацию имеет Sinon об объекте экспорта, созданном Webpack:
мы видим множество методов доступа к свойствам (получателей), но без фактического выполнения каждого из них мы не узнаем, что возвращает каждый получатель. Скрывает ли экспортированное свойство функцию или значение? Невозможно сказать. Это знания, которыми владеет только тот, кто проводит тест (и Webpack в этом отношении).

Так что, возможно, это ответ на ваш вопрос «Почему я должен волноваться?»: Потому что вы единственный, кто знает, чего от вас ждут.

Разумеется, это можно исправить, оснастив Webpack (с помощью плагина) дополнительными подсказками к объектам экспорта о том, что скрывается за геттерами, но опять же: это детали реализации, которые нам не нужны в ядре Sinon, но это может быть приятный дополнительный пакет (например, sinon-test или sinon-as-as-as-обещано в прошлом), который добавляет функциональность, поддерживаемую заинтересованным сообществом.

Или ... просто обойдите проблему полностью, используя швы ссылок (прокси-соединение, повторное подключение и т. Д.). В конце концов, эта проблема касается загрузки модулей, которой Sinon не занимается и для которой существуют специализированные продукты.

Наличие официального плагина Sinon для того или иного веб-пакета может быть хорошим делом, чтобы люди могли легко найти решение распространенной проблемы с веб-пакетом 4.

Правда, для этого стоит подумать о добавлении репо. А также некоторые документы ... Надеюсь, @lzhaki не будет возражать, если мы

@ fatso83 Этот плагин ? Все твое.

Я был бы рад помочь здесь - я написал плагин nodemon webpack , поэтому я, вероятно, смогу написать тесты и быстрее справиться с совместимостью с webpack 3.

Сказав это, я все еще не уверен, что configurable: false - правильный выбор команды Webpack. Возможно, мы поднимем вопрос о Webpack, прежде чем начинать новое репо.

@Izhaki

Сказав это, я все еще не уверен, что configurable: false - правильный выбор команды Webpack.

Речь идет о создании плагина Sinon для веб-пакетов, не изменяющего основные функции веб-пакета. Плагин Sinon webpack является добровольным по своей природе, поскольку те, кто использует Sinon, должны установить и добавить плагин в свои конфигурации webpack.

@ Ижаки всегда нужна помощь. Если вы создаете минимальное репо под названием sinon-webpack-plugin , то все, что, по вашему мнению, должно быть там, желательно вместе с каким-либо тестом, который функционально проверяет, что полученный транспилированный файл может быть протестирован Sinon ( pretest step в package.json который строит, test step проверяет результат с помощью Sinon), мы в мгновение ока создадим его в рамках организации Sinon! Это, конечно, автоматически отнесет вас к исходным коммитам.

Привет, ребята, я попытался использовать рекомендованный плагин, но все еще получаю сообщение об ошибке TypeError: Cannot redefine property: Я что-то упускаю? Я просто позвонил в конфигурацию своего веб-пакета:

 plugins: [
    new AllowMutateEsmExports()
  ]

Это не работает с последней версией Webpack. Мы застряли на "webpack": "4.8.1" где он все еще работает.

Это никогда не кончится, лол. Что ж, я думаю, пора начать использовать шутку или прекратить использовать веб-пакет в моих тестах.

Это глобальная проблема - она ​​касается соблюдения стандарта, и независимо от того, что вы будете использовать, вы столкнетесь с одной и той же проблемой (найдите там ту же самую проблему в репозитории jest). Вы не можете издеваться над модулями es.

Ничего общего с Sinon, Webpack или Jest.

Вы не можете издеваться над модулями es.

Это несколько спорно :-) Да, действительно, если вы следуете стандартам, это абсолютно невозможно, поскольку модули ES по стандарту не позволяют этого, но у вас есть такие вещи, как ESM, нарушающие правила :-) Итак, игнорируя WebPack на секунду, выполнение mocha --register esm my-module-test.es6 обычно позволяет вам это сделать (поскольку его опция mutableNamespace по умолчанию истинна).

Теперь, что касается проблемы, я просто собираюсь спросить @joepuzzo о том же, что я сделал в связанной ветке WebPack :
Зачем нужен webpack для запуска тестов? Почему вы не можете просто запустить их, используя Mocha напрямую, чтобы избежать всех хлопот? Он поддерживает преобразования времени выполнения с использованием Babel, поэтому веб-пакет вряд ли когда-либо понадобится:

mocha --require @babel/register test/**/*.js

Я ответил в исходном выпуске.

Зачем нужен webpack для запуска тестов? Почему вы не можете просто запустить их, используя Mocha напрямую, чтобы избежать всех хлопот?

Говоря за себя, я тестирую интерфейсный код в браузере. Это дает мне среду, в которой я действительно могу проверить пользовательский интерфейс, который я тестирую.

@ steve-taylor Я использовал Карму для достижения этого раньше. Сам по себе Webpack не делает ничего подобного; он просто создает набор, который можно использовать в ваших тестах. Есть ли конкретный загрузчик / плагин, который вы используете для запуска тестов в браузере?

Я переношу проект Vue из Vue CLI и, должно быть, обновил Webpack в процессе или что-то в этом роде (или, может быть, это произошло из-за того, что я удалил Babel?). Это привело к тому, что Sinon перестала работать без ошибок. Это просто не сработало. Очень запутанно.

На поиск этой проблемы у меня ушло больше суток. Я не стремлюсь полностью удалять Webpack (пока), потому что тогда мне придется создать совершенно новую систему сборки, которая выполняет компиляцию Typescript, независимо от того, что делает темная магия vue-loader и т. Д. И т. Д.

Итак, чтобы убедиться, что я правильно понимаю:

  1. Webpack пытается эмулировать модули ES6 (ESM).
  2. В последних версиях это так:
/******/    // define getter function for harmony exports
/******/    __webpack_require__.d = function(exports, name, getter) {
/******/        if(!__webpack_require__.o(exports, name)) {
/******/            Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/        }
/******/    };

...

/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "globalState", function() { return globalState; });
  1. Поскольку он не вызывает Object.defineProperty() с configurable: true , это означает, что экспорт globalState доступен только для чтения, поэтому Sinon не может его заглушить.

  2. Решение Ижаки - добавить плагин Webpack, который заставляет его использовать configurable: true . К сожалению, это также означает, что вам нужно изменить свой тестовый код с

sinon.stub(logger, 'createLogger').returns(loggerStub);

к

    Object.defineProperty(logger, 'createLogger', {
      writable: true,
      value: sinon.stub().returns(loggerStub)
    });

Изменить: также обратите внимание, что связанный плагин использует поиск и замену строк, что не очень надежно, что подтверждается тем фактом, что он не будет работать в текущем Webpack, потому что он пытается заменить configurable: false на configurable: true , но configurable: false теперь неявно (см. Выше).

  1. Webpack не будет добавлять параметр, разрешающий изменяемые модули ES6, потому что технически это несовместимо со стандартом.

  2. Обходной путь, который я пока не совсем понимаю, - это использовать Babel (т.е. добавить babel-loader в конфигурацию вашего webpack). Хотя это кажется дерьмом.

Я считаю, что лучшим решением для Sinon было бы обнаружение этих ситуаций (а не просто молчаливый сбой), а затем попытка использовать вместо этого метод Object.defineProperty() . Если это не удается, он должен вывести сообщение об ошибке с советом добавить плагин Izhaki AllowMutateEsmExports в конфигурацию Webpack.

Это примерно так?

Хорошо, у меня возникла идея просто сделать все свойства экспорта изменяемыми, аналогично решению Ижаки, но так, чтобы не требовалось никаких изменений в коде. Все еще использую хитрый поиск и замену на основе строк.

class MutableModulesHackPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap("MutableModulesHackPlugin", compilation => {
      compilation.mainTemplate.hooks.requireExtensions.tap(
        "MutableModulesHackPlugin",
        source => {
          let replaced = false;
          const newSource = source.replace(
            "Object.defineProperty(exports, name, { enumerable: true, get: getter });",
            () => {
              replaced = true;
              return `
  Object.defineProperty(exports, name,
    {
      enumerable: true,

      // Make it so that we can call Object.defineProperty() on this property
      // in the setter.
      configurable: true,

      // The original getter. Unfortunately we can't just do
      // exports[name] = getter() because the getter returns an object that
      // defined at the point that this function is called.
      get: getter,

      // When someone modifies this property, change the getter to return the
      // new value.
      set: val => {
        Object.defineProperty(exports, name,
          {
            get: () => val,
          }
        );
      },
    },
  );
`;
            },
          );
          if (!replaced) {
            throw new Error(
              "Couldn't find the required 'Object.defineProperty' string in Webpack output",
            );
          }
          return newSource;
        },
      );
    });
  }
}

К сожалению, sinon.stub() прежнему ничего не делает (и по-прежнему не сообщает об ошибках), но если я заменю

sinon.stub(MyModule, "aFunction").returns(42);

с участием

MyModule.aFunction = () => 42;

тогда это действительно работает! Я бегло ознакомился с исходным кодом Sinon, и мне кажется, что он вообще не может имитировать свойства getter / setter. Эта строка делает неправильные вещи:

var func = typeof actualDescriptor.value === "function" ? actualDescriptor.value : null;

Может быть. Я еще не совсем понимаю код Sinon - он довольно сложный и почти полностью не прокомментирован. : - /

Хорошо, я отказался от попыток выяснить, как изменить Sinon, чтобы он автоматически заглушал свойства, которые являются сеттерами / получателями, поэтому я просто создал оболочку, которая делает это вне Sinon:

function stubImport(object: any, property: string) {
  const prop = Object.getOwnPropertyDescriptor(object, property);
  if (prop !== undefined && prop.get !== undefined) {
    const value = sinon.stub();
    Object.defineProperty(object, property, { value });
    return value;
  }
  return sinon.stub(object, property);
}

Используйте это вместо sinon.stub(Module, "export") , а затем используйте MutableModulesHackPlugin выше, и, похоже, это сработает. Бит set самом деле является необязательным в плагине MutableModulesHackPlugin потому что мы его не используем, поэтому вы можете упростить его до следующего:

class MutableModulesHackPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap("MutableModulesHackPlugin", compilation => {
      compilation.mainTemplate.hooks.requireExtensions.tap(
        "MutableModulesHackPlugin",
        source => {
          let replaced = false;
          const newSource = source.replace(
            "Object.defineProperty(exports, name, { enumerable: true, get: getter });",
            () => {
              replaced = true;
              return "Object.defineProperty(exports, name, { enumerable: true, get: getter, configurable: true });";
            },
          );
          if (!replaced) {
            throw new Error(
              "Couldn't find the required 'Object.defineProperty' string in Webpack output",
            );
          }
          return newSource;
        },
      );
    });
  }
}
    const value = sinon.stub();
    Object.defineProperty(object, property, { value });

Хм, к сожалению, хотя это и работает, sinon.restore() не восстанавливает его должным образом. Полагаю, это не было неожиданностью. Кто-нибудь знает способ сделать это таким образом, чтобы он работал с restore() ?

Ага, я нашел решение! Просто превратите получатель в обычное свойство на основе значений.

export function stubImport(object: Record<string, any>, property: string) {
  const prop = Object.getOwnPropertyDescriptor(object, property);
  if (prop !== undefined && prop.get !== undefined) {
    Object.defineProperty(object, property, {
      value: object[property],
      writable: true,
      enumerable: true,
    });
  }
  return sinon.stub(object, property);
}

Объедините это с MutableModulesHackPlugin выше, а затем используйте stubImport(foo, bar) вместо sinon.stub(foo, bar) (для элементов уровня импорта), и тогда все будет работать правильно!

Было бы неплохо, если бы Sinon предоставил плагин для веб-пакетов, например MutableModulesHackPlugin , а также автоматически определял их при запуске sinon.stub() , но я могу жить с этим решением.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги