Sinon: Erros com .stub () e .spy () causados ​​por getters / setters

Criado em 5 abr. 2018  ·  31Comentários  ·  Fonte: sinonjs/sinon

O que você esperava que fosse acontecer?

No @thumbtack , estamos no processo de atualização de nossa construção para o Webpack 4. Como parte disso, alguns de nossos testes de unidade começaram a falhar. Rastreamos até os casos em que estamos usando as funcionalidades .spy e .stub do Sinon em módulos que são exportados usando uma exportação ES6 não padrão da forma export function foo . Investigando, parece que sob o capô o Webpack cria getters e setters para essas exportações para sua nova implementação de módulos Harmony. Isso mudou da versão 3 para a versão 4.

Também fomos capazes de reproduzir isso independentemente do Webpack, tentando criar um stub de um objeto simples que tenha um getter ou setter.

O que realmente acontece

Ao usar .stub , o stubbing funciona, mas uma chamada posterior para .restore não, e a declaração falha.

Ao usar .spy , o seguinte erro é gerado: TypeError: Attempted to wrap undefined property foo as function . Por alguma razão, Sinon pensa que uma propriedade é indefinida quando também existe como um getter.

Como reproduzir

Eu criei um repo que tem uma reprodução mínima dos problemas stub e spy , com as versões mais recentes do Webpack e do Sinon. Ele também tem um caso base que mostra que esse problema não ocorre em objetos que não são importados dessa forma e, portanto, não usam setters.

Você pode clonar o repo aqui: https://github.com/lavelle/sinon-stub-error

Execute yarn install e, em seguida, execute

  • yarn pass para ver o caso base
  • yarn fail para ver o caso de stub com falha
  • yarn spy para ver o caso do espião em falha

Desde já, obrigado! Teremos prazer em enviar um PR para resolver isso, se você puder nos indicar a direção certa.

cc @bawjensen @dcapo

Property accessors

Comentários muito úteis

@mroderick

OK. Vamos fazer isso passo a passo.

As exportações do ESM usam vinculação imutável .

Então, quando você escreve:

export const x = value;

O código real emitido acabará chamando:

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

onde name é x e getter é uma função que retorna value .

Quando você escreve:

import { x } from './X';

O que é importado é o getter de x , não o valor. Além disso, como não há set no descritor de propriedade, essa importação é somente leitura (imutável).

É por isso que Sinon.Stub(object, "method") não funciona (deve lançar TypeError no modo estrito).

No entanto, enquanto o bundler definir configurable: true , a exportação ainda será somente leitura, mas pode ser substituída por algo como este:

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

Que é o que o sinon precisa fazer para oferecer suporte às importações de stubbing es6.


Não tenho 100% de certeza do que se trata exatamente - os comentários parecem divergir do problema original. Mas basicamente, se este tíquete relatar a incapacidade de stub es6 importações, então esta é a solução.

Todos 31 comentários

Parece que isso pode estar relacionado a https://github.com/sinonjs/sinon/issues/1741 , mas isso não especifica que há um erro gerado. Idealmente, também não precisaríamos usar métodos Sinon especiais para simular Getters e poderíamos usar os métodos .spy e .stub existentes. Imagino que esse problema se tornará mais prevalente à medida que mais projetos forem atualizados para usar o Webpack 4.

Aqui está um exemplo de como o Webpack compila o código nos bastidores.

Um exemplo de arquivo ES6 com uma API como

export function get(url, data, options) {

}

export function post(url, data, options) {

}

export function getJSON(url, data, options) {

}

Produz este código no 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; }

E este código no 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; }

Como você pode ver, a implementação do módulo agora usa Object.defineProperty para criar funções getter dinamicamente para funções exportadas dessa maneira. Não sei por que isso mudou, mas provavelmente é para oferecer suporte a alguns novos recursos do sistema de módulo. Não acho que seja um bug no Webpack, porque o código funciona bem em todos os navegadores e no Node. Ele só tem problemas ao usá-lo com o Sinon.

Encontrei a seção nos documentos dizendo

API Stub
Se você precisar criar stub getters / setters ou propriedades não funcionais, deverá usar sandbox.stub

No entanto, mudei para sandbox.stub e recebi o mesmo erro.

A única coisa que corrige para mim é substituir

import * as obj from './index';

com

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

já que isso impede que o Webpack crie getters no código compilado.

Eu prefiro não fazer isso em toda a nossa base de código, entretanto, import * deve ser uma forma válida de importar coisas. Sei que isso é parcialmente um problema do Webpack, mas considerando o quão amplamente usado na comunidade, seria bom corrigi-lo no Sinon para que as duas ferramentas sejam compatíveis.

Idealmente, também não precisaríamos usar métodos Sinon especiais para simular Getters e poderíamos usar os métodos .spy e .stub existentes. Imagino que esse problema se tornará mais prevalente à medida que mais projetos forem atualizados para usar o Webpack 4.

Nós também, mas esse problema ainda não foi resolvido.

Stubbing getters / setters foi adicionado em sinon@2 de # 1297

A próxima versão do Sinon npm i sinon<strong i="5">@next</strong> --save-dev usa uma sandbox padrão em sinon , o que significa que você não terá que usar explicitamente sandbox .

No entanto, ele ainda usa stub.get() e stub.set() para getters / setters.

Ping @lucasfcosta : você tem alguma ideia para esse assunto?

Como @lavelle mencionou. A importação de esm no Webpack 4 usa ligações imutáveis ​​com a seguinte configuração:

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

Tendo o mesmo problema, escrevi um pequeno plugin webpack para substituir configurable: false por configurable: true , para que possamos usar sinon para criar um stub da seguinte maneira:

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

No mesmo problema (consulte a última seção), argumentei que o Webpack deveria considerar configurable: true vez de configurable: false (portanto, não há necessidade do plug-in). Não sei se eles vão aceitar ou não.

Mas, de qualquer forma, pode ser sensato considerar Sinon.Stub(object, "method") chamando Object.getOwnPropertyDescriptor e se tiver a marca de uma importação de ESM, conecte o esboço (para que não tenhamos que escrever o código acima )

@Izhaki Não tenho certeza se entendi o que você está propondo como solução.

Você se importaria de criar um exemplo executável que demonstrasse sua ideia, de uma forma que não seja específica para nenhum carregador de módulo (Webpack, Rollup), mas seja apenas JS puro e pode ser usado diretamente em tempos de execução de suporte de ESM, como em navegadores permanentes?

@mroderick

OK. Vamos fazer isso passo a passo.

As exportações do ESM usam vinculação imutável .

Então, quando você escreve:

export const x = value;

O código real emitido acabará chamando:

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

onde name é x e getter é uma função que retorna value .

Quando você escreve:

import { x } from './X';

O que é importado é o getter de x , não o valor. Além disso, como não há set no descritor de propriedade, essa importação é somente leitura (imutável).

É por isso que Sinon.Stub(object, "method") não funciona (deve lançar TypeError no modo estrito).

No entanto, enquanto o bundler definir configurable: true , a exportação ainda será somente leitura, mas pode ser substituída por algo como este:

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

Que é o que o sinon precisa fazer para oferecer suporte às importações de stubbing es6.


Não tenho 100% de certeza do que se trata exatamente - os comentários parecem divergir do problema original. Mas basicamente, se este tíquete relatar a incapacidade de stub es6 importações, então esta é a solução.

Aqui está um exemplo mais concreto de como habilitamos importações es6 de stub:

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

Você está certo, esse problema está em todo o lugar. Todas as coisas de getter / setter / sandbox prejudicaram o ponto principal. Há confusão em vários estágios, mas acho que você resumiu, @lzhaki.

  1. Este não é um bug no Sinon.
  2. Os documentos sobre como getters / setters são criados não foram lidos, portanto, as reclamações sobre o funcionamento deles são mais sobre o uso incorreto (também: haveria falha se configurável fosse falso, conforme mencionado)
  3. É superficialmente uma solicitação de recurso para tornar mais fácil testar o novo código transpilado do Webpack. Ajustar o Sinon para formatos de ferramentas e peculiaridades não é tão interessante da minha perspectiva, e algo que normalmente deve ser feito no estágio de construção (fora do Sinon), como o que foi feito usando seu plugin para tornar os acessadores configuráveis.

Eu simpatizo com a ideia de tornar o Sinon mais "auto-legal", mas não tenho certeza se a correção proposta é suficiente ou à prova de erros. O que constitui "as marcas de um módulo ESM"? Como podemos detectar com segurança que estamos lidando com ESM transpilado, não com algum objeto geral que por acaso tem um getter? Já temos suporte explícito para substituir acessadores, então isso não funcionará.

Poderíamos adicionar métodos adicionais, é claro, chamados sinon.stubImport ou algo assim, mas é um método de uso e intervalo de tempo muito limitados.

Lembre-se de que isso só funcionará / fará sentido em um mundo transpilar para ES5, pois os módulos ES são realmente imutáveis. Detectamos isso explicitamente e dizemos que não podemos oferecer suporte, portanto, https://github.com/sinonjs/sinon/blob/master/test/es2015/module-support-assessment-test.mjs.

Uma vez que as pessoas terminam com o ESM nativo e o carregamento HTT2, tornando o empacotamento obsoleto, todos esses hacks vão embora.

Eu acho que simplesmente adicionar recursos de espionagem por padrão ao nosso stub de acessador existente é uma solução melhor que também seria utilizável de forma mais geral. Veja # 1741 para uma discussão sobre isso.


Estou de férias sem um computador, então não estou em posição de testar o código, mas acho que as etapas necessárias para o autor da postagem original atingir o objetivo pretendido são simplesmente estas:

  1. Use o plugin Webpack mencionado para tornar as exportações transpiladas configuráveis
  2. Faça o stub da exportação assim:
    sinon.stub(myModule, 'foo').get( ()=>42 )

Conforme discutido em # 1741, o stub aprovado atualmente fornece apenas comportamento, não espionagem. Até que alguém (@RoystonS?) Expanda a API, você precisará passar uma função espionada como o getter para verificar as interações. Melhores docs seriam legais, concordou ...

Como esses pontos devem responder à questão original, considero que isso tem a ver com documentação insuficiente, não um bug. Sinta-se à vontade para fornecer melhorias :-)

Links:

@ fatso83

Embora o que você diga faça muito sentido, permita-me contra-argumentar.

O que aconteceu?

Queríamos nos beneficiar com a trepidação da árvore, então mudamos o Typescript para emitir módulos es6 em vez de es5 - foi aí que todos os nossos sinon.stub testes falharam.

Este é apenas o começo

Eu ficaria muito surpreso se ESM não prevalecer em um futuro próximo. Há muito de bom nisso em comparação com outros paradigmas.

Provavelmente, você terá mais e mais pessoas abordando esse problema ou abrindo uma nova edição sobre esse assunto.

Não se trata de webpack

A implementação de esm import como getter não parece ser uma escolha feita pelo webpack - parece que esta é a maneira de cumprir o padrão.

Portanto, a seguir, substitua 'webpack' e 'formato de ferramentas' por 'o padrão':

É superficialmente uma solicitação de recurso para tornar mais fácil testar o novo código transpilado do Webpack. Ajustar o Sinon para formatos de ferramentas e peculiaridades não é tão interessante do meu ponto de vista ...

Esta nunca foi uma solução proposta

Eu simpatizo com a ideia de tornar o Sinon mais "auto-legal", mas não tenho certeza se a correção proposta é suficiente ou à prova de erros.

O código que forneci foi apenas um exemplo básico - longe de ser uma 'solução' no que diz respeito ao sinon.

Cuidado com a superfície da API

Como podemos detectar com segurança que estamos lidando com ESM transpilado, não com algum objeto geral que por acaso tem um getter? Já temos suporte explícito para substituir acessadores, então isso não funcionará.

Bem, há outra maneira de ver isso ... por que há um suporte explícito para acessadores? Por que me importo se é um objeto com um acessador ou apenas uma função?

Por que devo usar .return em um caso e .get em outro? É um detalhe de implementação para mim como redator do teste.


De qualquer forma, espero estar errado sobre tudo isso, e esse é realmente um problema de muito poucas pessoas que pareço acreditar que esteja. Teremos que esperar para ver.

A implementação de esm import como getter não parece ser uma escolha feita pelo webpack - parece que esta é a maneira de cumprir o padrão.

Sim, é uma forma de cumprir o padrão em ambientes que não suportam / implementam Módulos ES, e onde você deseja o efeito de um sistema de módulo em um ambiente que não o suporta nativamente. O código transpilado provavelmente será o padrão de fato de como os Módulos ES serão consumidos por um bom tempo, até que o suporte seja onipresente. Mas não é a coisa real.

Isso é uma boa coisa para testar empregando mutação de alvos, pois os verdadeiros Módulos ES não podem ser fragmentados .

Direcionar explicitamente o formato de saída do Webpack não parece uma boa maneira de gastar recursos de manutenção, pois é um alvo móvel que talvez não seja necessário em alguns anos, mas o objetivo de tornar a API mais fácil de usar é bom . Como você afirmou, por que você deveria se importar. No momento, não estou totalmente certo se / quais motivos técnicos tínhamos para fazer a API que fizemos, mas ATM, eu realmente não consigo ver por que não podemos nos safar com o desnecessário esboço fictício usado hoje em dia para fazer o stub do acessório. Menciono algumas desvantagens dessa simplificação mais adiante, que pode ser a razão pela qual o autor original da funcionalidade do Sinon 2+ tomou as decisões de API que ele fez.

Os descritores de propriedade estão salvando um superconjunto do valor da propriedade, certo? Portanto, deve ser utilizável onde quer que salvemos valores (como a função original) hoje. Sempre armazenando o descritor de propriedade original e usando descritores de propriedade ao atribuir novos stubs, devemos ser capazes de reutilizar a mesma lógica (embora eu suspeite que as alterações na base de código do Sinon podem ser bastante invasivas ...). Isso removeria o suporte explícito para diferentes lógicas de setter e getter, mas isso poderia ser resolvido no stub, verificando se ele foi usado como setter ou getter.

Isso tornaria a API mais agradável para alguns casos (embora o stub do acessador seja relativamente raro hoje), mas também tornaria outros casos potencialmente confusos: especialmente o stub dos módulos empacotados do Webpack. Para ver isso, considere quais informações a Sinon possui sobre o objeto de exportação feito pelo Webpack:
vemos muitos acessadores de propriedade (getters), mas sem realmente executar cada um, não saberíamos o que é retornado por cada getter. A propriedade exportada está ocultando uma função ou um valor? Impossível saber. Esse é o conhecimento detido apenas por quem está implementando o teste (e Webpack para esse assunto).

Então essa talvez seja a resposta à sua pergunta "Por que eu deveria me importar?": Porque você é o único que sabe o que é esperado.

Isso poderia ser remediado, é claro, instrumentando o Webpack (usando um plug-in) para adicionar dicas adicionais aos objetos de exportação sobre o que está se escondendo por trás dos getters, mas novamente: esses são detalhes de implementação que não queremos no núcleo do Sinon, mas poderia ser um bom pacote complementar (como o sinon-test ou o sinon-as-prometido no passado) que adiciona funcionalidade, mantida pela comunidade interessada.

Ou ... simplesmente ignore o problema completamente, usando junções de

Ter um plugin oficial do webpack da Sinon para fazer isso ou aquilo pode ser uma boa coisa para que as pessoas possam ter uma solução fácil de encontrar para um problema comum do webpack 4.

É verdade que devemos considerar a adição de um repositório para isso. Bem como alguns documentos ... Felizmente, @lzhaki não se importará se

@ fatso83 Este plugin ? Todo seu.

Eu ficaria feliz em ajudar aqui - eu escrevi o plugin nodemon webpack , então provavelmente seria capaz de escrever os testes e lidar com a compatibilidade do webpack 3 mais rápido.

Dito isso, ainda não tenho certeza se configurable: false é a escolha certa da equipe do Webpack. Talvez devamos levantar um problema no Webpack antes de nos aventurarmos a iniciar um novo repo.

@Izhaki

Dito isso, ainda não tenho certeza se configurable: false é a escolha certa da equipe do Webpack.

Trata-se de criar um plugin do webpack do Sinon sem alterar a funcionalidade do webpack do núcleo. Um plug-in do Sinon webpack é opcional por natureza, já que aqueles que usam o Sinon precisam instalar e adicionar o plug-in às suas configurações do webpack.

A ajuda de sinon-webpack-plugin , tudo o que você sentir que precisa estar lá, de preferência junto com algum tipo de teste que funcionalmente certifique-se de que o arquivo transpilado resultante pode ser testado pelo Sinon ( pretest etapa em package.json que constrói, test etapa que testa o resultado usando Sinon), vamos bifurcá-lo em um instante sob a organização Sinon! Fazer isso irá, é claro, atribuir automaticamente a você por meio dos commits originais.

Olá pessoal, tentei usar o plugin recomendado e ainda recebo o erro TypeError: Cannot redefine property: Estou faltando alguma coisa? Eu simplesmente chamei o seguinte na configuração do meu webpack:

 plugins: [
    new AllowMutateEsmExports()
  ]

Isso não funciona com a versão mais recente do Webpack. Estamos presos em "webpack": "4.8.1" onde ainda funciona.

Isso nunca acaba lol. Bem, acho que é hora de começar a usar o jest ou parar de usar o webpack em meus testes.

Este problema é global - é sobre a adesão ao padrão e, independentemente do que você usar, você encontrará o mesmo problema (pesquise o repositório de jest para o mesmo problema lá). Você não pode simular módulos es.

Nada a ver com Sinon, Webpack ou Jest.

Você não pode simular módulos es.

Isso é um tanto discutível :-) Sim, de fato, se você está seguindo os padrões, isso é absolutamente impossível, já que os Módulos ES de acordo com o padrão não permitem isso, mas você tem coisas como ESM quebrando as regras :-) Então, ignorar WebPack por um segundo, fazendo mocha --register esm my-module-test.es6 normalmente permitiria que você fizesse isso (já que sua opção mutableNamespace é verdadeira por padrão).

Agora, para o problema aqui, vou apenas perguntar a @joepuzzo a mesma coisa que fiz no tópico do WebPack vinculado :
Por que o webpack é necessário para executar os testes? Por que você não pode simplesmente executá-los usando o Mocha diretamente para evitar todo o aborrecimento? Ele suporta transformações de tempo de execução usando Babel, então o webpack quase nunca é necessário:

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

Eu respondi no problema original.

Por que o webpack é necessário para executar os testes? Por que você não pode simplesmente executá-los usando o Mocha diretamente para evitar todo o aborrecimento?

Falando por mim, eu testo o código de front-end no navegador. Isso me dá um ambiente no qual posso realmente inspecionar a IU que estou testando.

@steve-taylor Eu costumava usar o Karma para fazer isso antes. O Webpack em si não faz isso; ele apenas cria um bunde que pode ser usado em seus testes. Existe um carregador / plug-in específico que você usa para executar seus testes no navegador?

Estou migrando um projeto do Vue do Vue CLI e devo ter atualizado o Webpack no processo ou algo assim (ou talvez tenha sido porque removi o Babel?). Isso fez com que o Sinon parasse de funcionar sem erros. Simplesmente não funcionou. Muito confuso.

Levei mais de um dia para encontrar esse problema. Não estou interessado em remover completamente o Webpack (ainda) porque então terei que fazer um novo sistema de compilação que faça compilação de Typescript, qualquer magia negra que vue-loader faça, etc.

Então, para ter certeza de que entendi corretamente:

  1. Webpack está tentando emular Módulos ES6 (ESM).
  2. Em versões recentes, funciona assim:
/******/    // 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. Como não chama Object.defineProperty() com configurable: true , isso significa que a exportação globalState é somente leitura, portanto, Sinon não pode fazer o stub.

  2. A solução de Izhaki é adicionar um plug-in Webpack que o faça usar configurable: true . Infelizmente, também significa que você deve alterar seu código de teste, de

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

para

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

Edit: Oh, também observe que o plug-in vinculado usa uma pesquisa e substituição de string, que não é muito robusta, conforme comprovado pelo fato de que não funcionará no Webpack atual porque tenta substituir configurable: false por configurable: true , mas configurable: false agora está implícito (veja acima).

  1. O Webpack não adicionará uma opção para permitir módulos ES6 mutáveis porque tecnicamente isso não é compatível com o padrão.

  2. Uma solução alternativa que eu realmente não entendo ainda é usar o Babel (ou seja, adicionar babel-loader à configuração do seu webpack). Isso parece uma merda, no entanto.

Acho que a melhor solução seria o Sinon detectar essas situações (em vez de apenas falhar silenciosamente) e, em seguida, tentar usar o método Object.defineProperty() . Se isso falhar, ele deve exibir um erro aconselhando você a adicionar o plug-in AllowMutateEsmExports de Izhaki à configuração do Webpack.

Isso está certo?

Ok, eu tive a ideia de apenas fazer todas as propriedades mutáveis ​​das exportações, semelhante à solução de Izhaki, mas de forma que não fosse necessário fazer alterações no código. Ainda usando pesquisa e substituição baseada em string hacky.

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;
        },
      );
    });
  }
}

Infelizmente sinon.stub() ainda não faz nada (e ainda não relata erros), mas se eu substituir

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

com

MyModule.aFunction = () => 42;

então funciona! Dei uma olhada rápida no código-fonte do Sinon e parece que ele não pode simular propriedades getter / setter. Esta linha não faz a coisa certa:

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

Pode ser. Eu não entendo totalmente o código do Sinon ainda - é bastante complicado e quase totalmente sem comentários. : - /

Ok, desisti de tentar descobrir como modificar o Sinon para que ele crie automaticamente um stubs de propriedades que são setters / getters, então acabei de fazer um wrapper que faz isso fora do 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);
}

Use isso em vez de sinon.stub(Module, "export") e, em seguida, use MutableModulesHackPlugin acima e parece funcionar. O bit set é realmente opcional no plug-in MutableModulesHackPlugin porque não o usamos, portanto, você pode simplificá-lo desta forma:

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

Hmm, infelizmente, enquanto isso funciona, não é restaurado corretamente por sinon.restore() . Não é inesperado, suponho. Alguém conhece uma maneira de fazer isso de uma forma que funcione com restore() ?

Aha, encontrei uma solução! Basta transformar o getter em uma propriedade baseada em valor normal.

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

Combine isso com MutableModulesHackPlugin acima, e então use stubImport(foo, bar) vez de sinon.stub(foo, bar) (para itens de nível de importação), e então tudo funcionará corretamente!

Seria bom se a Sinon fornecesse um plugin webpack como MutableModulesHackPlugin , e também os detectasse automaticamente quando você executa sinon.stub() , mas posso viver com esta solução.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

ALeschinsky picture ALeschinsky  ·  4Comentários

byohay picture byohay  ·  3Comentários

sudhirbits picture sudhirbits  ·  4Comentários

fearphage picture fearphage  ·  3Comentários

stevenmusumeche picture stevenmusumeche  ·  3Comentários