Sinon: Ideia para marcos futuros

Criado em 13 set. 2017  ·  31Comentários  ·  Fonte: sinonjs/sinon

Fundo

sinon.stub / sandbox.stub se tornou a pia da cozinha
comportamento configurável com problemas que geralmente são difíceis de encontrar e corrigir sem regressões.

Eu acho que a causa raiz das dificuldades é que stub tem muitas responsabilidades.

Além disso, stub também tem um uso problemático causado pelo fato de que o comportamento é definido após ter sido criado e pode ser redefinido várias vezes.

var myStub;

beforeEach(function(){
    myStub = sinon.stub().resolves('apple pie :)');
});

// several hundred lines of tests later
myStub = sinon.stub().rejects('no more pie :(');

// several hundred lines of tests later
// what behaviour does myStub currently have? Can you tell without 
// reading the entire file?
// can you safely change the behaviour without affecting tests further 
// down in the file?

E depois há os cenários mais confusos

var myStub = sinon.stub()
                .withArgs(42)
                .onThirdCall()
                .resolves('apple pie')
                .rejects('no more pie')

O que isso faz mesmo?

Em vez de continuar a adicionar mais responsabilidades a stub , proponho que, em vez disso, apresentemos novos membros a sinon , que têm um escopo muito mais restrito .

O mais importante que posso pensar seria um substituto imutável para uma função.

Podemos então descobrir mais tarde o que vamos fazer com as propriedades (como um membro de responsabilidade novo, separado e único).

sinon.fake

Um fake (o valor de retorno de chamar sinon.fake ) é um Function puro e imutável . Ele faz uma coisa, e uma coisa só. Ele tem o mesmo comportamento em cada chamada. Ao contrário stub , seu comportamento não pode ser redefinido. Se você precisar de um comportamento diferente, faça um novo fake .

Responsabilidade única

Uma falsificação pode ter uma dessas responsabilidades

  • resolver um Promise para um valor
  • rejeitar um Promise para um Error
  • retornar um valor
  • jogue um Error
  • valor(es) de rendimento para um retorno de chamada

Se você quer/precisa de efeitos colaterais, e ainda quer a interface do espião, então use a função real, use um stub ou faça uma função personalizada

sinon.replace(myObject, myMethod, sandbox.spy(function(args) {
    someFunctionWithSideEffects(args);
});

Lança erros generosamente

Será generoso ao lançar erros quando o usuário tentar criá-los/usá-los de maneiras não suportadas.

// will throw TypeError when `config` argument has more than one property
const fake = sinon.fake({
    resolves: true, 
    returns: true
});

Usa a API espiã

Exceto .withArgs , pois isso viola a imutabilidade

Ideias de uso

// will return a Promise that resolves to 'apple pie'
var fake = sinon.fake({resolves: 'apple pie'})

// will return a Promise that rejects with the provided Error, or 
// creates a generic Error using the input as message
var fake = sinon.fake({rejects: new TypeError('no more pie')});
var fake = sinon.fake({rejects: 'no more pie'});

// returns the value passed
var fake = sinon.fake({returns: 'apple pie'});

// throws the provided Error, or creates a generic Error using the 
// input as message
var fake = sinon.fake({throws: new RangeError('no more pie')});
var fake = sinon.fake({throws: 'no more pie'});

// replace a method with a fake
var fake = sinon.replace(myObject, 'methodName', sandbox.fake({
    returns: 'apple pie'
}))
// .. or use the helper method, which will use `sandbox.replace` and `
// sinon.fake`
var fake = sinon.setFake(global, 'methodName', {
    returns: 'apple pie'
});

// create an async fake
var asyncFake = sinon.asyncFake({
    returns: 'apple pie'
});

Sinônimos

Não sei se fake é o melhor substantivo para usar aqui, mas acho que devemos tentar manter a convenção de usar substantivos, e não nos desviarmos para adjetivos ou verbos.

Alterações propostas na API

Usar uma caixa de areia padrão

Isso é algo que venho considerando há algum tempo, por que não criamos uma sandbox padrão? Se as pessoas precisarem de sandboxes separadas, elas ainda poderão criá-las.

Devemos criar um sandbox padrão que seja usado para todos os métodos expostos via sinon.* .
Isso significa que sinon.stub se tornará o mesmo que sandbox.stub , o que removerá a limitação de poder stub propriedades usando sinon.stub .

sandbox.replace

Crie sandbox.replace e use isso para todas as operações que substituem qualquer coisa em qualquer lugar. Exponha isso como sinon.replace e use o sandbox padrão quando usado dessa maneira.

Isso provavelmente deve ter alguma validação de entrada séria, então só substituirá funções por funções, acessadores por acessadores, etc.

Feature Request Improvement Needs investigation pinned

Comentários muito úteis

OK, acho que temos um entendimento semelhante 👍

Apenas para reiterar, caso tenhamos perdido alguma coisa e para que outros colaboradores tenham o mesmo entendimento.

TL;DR

  • todas as substituições serão feitas por um novo utilitário: sandbox.replace (atualmente está em stub )
  • sinon terá um sandbox padrão, permitindo sinon.reset e sinon.restore (devemos apenas mesclar esses?)
  • sinon.fake — um substituto imutável e programável para funções que gravam todas as chamadas
  • sinon.spy
  • sinon.stub
// effectively a spy that has no target
const fake = sinon.fake()

// spy on a function
const fake = sinon.fake(console.log);
const fake = sinon.fake(function() { return 'apple pie'; });

// a shorthand construction of fake with behaviour
const fake = sinon.fake({
    returns: 'apple pie'
});

// replacing an existing function with a fake
var fakeLog = sinon.fake();
sandbox.replace(console, 'log', fakeLog);

Nesse estado, a proposta trata apenas de Function . Precisamos considerar o que fazer sobre propriedades e acessadores que não são de função. No mínimo, devemos ver se podemos limitar sandbox.replace para permitir apenas substituições sãs.

Todos 31 comentários

Ping @sinonjs/core

Boas sugestões, Morgana. Obrigado por trazer isso à tona. Também acho que a API stub é uma confusão e gosto de todas as suas sugestões. Aqui estão alguns pensamentos:

sinon.fake

Concordo que a imutabilidade é a chave aqui. No entanto, poderíamos permitir alguns casos de uso "sadios" que atualmente são possíveis com stubs.

Por exemplo, pode ser um caso de uso válido para render e retornar:

sinon.fake({
  yields: [null, 42],
  returns: true
})

Podemos verificar o que faz sentido e o que não faz.

Além disso, se dermos suporte callsThrough: true como uma configuração (que é inválida em combinação com qualquer uma das propriedades de comportamento), as novas falsificações também podem ser usadas em vez da API "spy". Isso seria mais autoexplicativo do que aprender o que "spy" e "stub" significam em Sinon-speak 😄

Usar uma caixa de areia padrão

Embora eu goste dessa ideia, isso significa que chamar sinon.restore() após um teste pode reverter algumas sobras de outros testes e levar a resultados surpreendentes - ou testes falhos que funcionaram antes. A coisa brilhante que isso permitiria é redefinir o sandbox global em beforeEach para melhorar o isolamento do teste. 👍

caixa de areia.substituir

Eu gosto muito disso. Eu entendo isso como um utilitário "deixe-me colocar essa coisa aí", certo?

Além disso, se dermos suporte a callsThrough: true como uma configuração (que é inválida em combinação com qualquer uma das propriedades de comportamento), as novas falsificações também podem ser usadas em vez da API "spy". Isso seria mais autoexplicativo do que aprender o que "spy" e "stub" significam em Sinon-speak 😄

Isso significaria que não precisaríamos de spy ou stub ?

caixa de areia.substituir

Eu gosto muito disso. Eu entendo isso como um utilitário "deixe-me colocar essa coisa aí", certo?

Sim, essa era a ideia. Em vez de sobrecarregar o mesmo método ( sinon.stub ) para fazer muitas, muitas coisas, tenha métodos explícitos que fazem apenas uma coisa

Como você destacou, a API fake provavelmente não suportará tudo o que é possível atualmente com espiões e stubs. Mas sim, acho que a API fake é uma oportunidade de unificar as funcionalidades stub e spy .

Embora eu goste dessa ideia, isso significa que chamar sinon.restore() após um teste pode reverter algumas sobras de outros testes e levar a resultados surpreendentes - ou testes falhos que funcionaram antes. A coisa brilhante que isso permitiria é redefinir o sandbox global em beforeEach para melhorar o isolamento do teste. 👍

É certamente uma mudança de ruptura, e não deve ser introduzida de ânimo leve.

Ao criar um fake , se você não passar uma configuração de comportamento, seria equivalente a um spy .

// ~spy, records all calls, has no behaviour
const fake = sinon.fake();

// ~stub, records all calls, returns 'apple pie'
const fake = sinon.fake({
    returns: 'apple pie'
});

Como você criaria um stub que não faz nada então?

Como você criaria um stub que não faz nada então?

Não sei se entendi bem sua pergunta, mas vamos lá

// a fake that has no behaviour
const fake = sinon.fake();

// put it in place of an existing method
sandbox.replace(myObject, 'someMethod', fake);

Ah, acho que entendi o que você quis dizer agora: A fake é sempre um stub . Quando você disse ~spy, records all calls eu entendi "chamadas para a função original". No entanto, o fake não tem conhecimento sobre a função que está substituindo – é isso que sandbox.replace faz.

Então, com isso em mente, aqui está outra proposta de como poderíamos dobrar a atual funcionalidade spy (como fazer chamadas) nas novas falsificações:

const fake = sinon.fake(function () {
   // Any custom function
});

A função dada seria chamada pelo fake. Essa API torna impossível misturá-la com outros comportamentos. Na verdade, um objeto de configuração criaria uma função que implementa o comportamento especificado e depois o passa para o fake.

A implementação sandbox.spy(object, method) poderia então se tornar isso:

const original = object[method];
const fake = sinon.fake(original);
sandbox.replace(object, method, fake);

Basicamente um one-liner 🤓

Sim. Depois de simplificar as coisas, você pode começar a remixar por diversão 🎉 e lucrar 💰

No entanto, se quisermos gravitar apenas usando fake e não mais usando spy e stub , provavelmente devemos deixar esses dois em paz.

Estou pensando na "próxima" API aqui. Você precisaria de sandbox.spy para ter a lógica de substituição em algum lugar. Pelo que entendi, isso deve ser compatível com versões anteriores. A implementação stub poderia então ser preterida.

Você precisaria do sandbox.spy para ter a lógica de substituição em algum lugar. Pelo que entendi, isso deve ser compatível com versões anteriores. A implementação de stub pode então ser preterida.

Não tenho certeza se sigo. Você poderia detalhar?

Certo. Pelo que entendi, você quer um substituto para a API stub complicada demais. A maneira como os stubs são implementados atualmente é criando um spy com uma função que implementa o comportamento. O que estou sugerindo é fazer a mesma coisa com a API fake e criar internamente um spy , mas não retornaríamos mais um comportamento, pois queremos nos livrar do encadeamento . Devolveríamos o espião. Isso torna a implementação fake uma alternativa para stub com a função retornada sendo compatível com todas as APIs Sinon atuais. Isso faz sentido ou estou perdendo alguma coisa?

OK, acho que temos um entendimento semelhante 👍

Apenas para reiterar, caso tenhamos perdido alguma coisa e para que outros colaboradores tenham o mesmo entendimento.

TL;DR

  • todas as substituições serão feitas por um novo utilitário: sandbox.replace (atualmente está em stub )
  • sinon terá um sandbox padrão, permitindo sinon.reset e sinon.restore (devemos apenas mesclar esses?)
  • sinon.fake — um substituto imutável e programável para funções que gravam todas as chamadas
  • sinon.spy
  • sinon.stub
// effectively a spy that has no target
const fake = sinon.fake()

// spy on a function
const fake = sinon.fake(console.log);
const fake = sinon.fake(function() { return 'apple pie'; });

// a shorthand construction of fake with behaviour
const fake = sinon.fake({
    returns: 'apple pie'
});

// replacing an existing function with a fake
var fakeLog = sinon.fake();
sandbox.replace(console, 'log', fakeLog);

Nesse estado, a proposta trata apenas de Function . Precisamos considerar o que fazer sobre propriedades e acessadores que não são de função. No mínimo, devemos ver se podemos limitar sandbox.replace para permitir apenas substituições sãs.

Isso significa que sinon.stub() e sinon.spy() serão preteridos no futuro em favor de sinon.fake() ou apenas refeitos internamente? Se sim, então estamos essencialmente nos movendo em direção ao pensamento de TestDouble . Não necessariamente uma coisa ruim, IMHO, mas pode valer a pena considerar que, se muitas pessoas acharem que precisarão substituir todas as chamadas de API Sinon de qualquer maneira por sinon.fake() , elas também podem usar outra biblioteca (embora isso significaria que eles perderiam todo o conhecimento existente da API da Sinon).

Isso significa que sinon.stub() e sinon.spy() serão descontinuados no futuro em favor de sinon.fake(), ou apenas refeitos internamente? Se sim, então estamos essencialmente nos movendo em direção ao pensamento de TestDouble.

Acho que se sobrepõe um pouco. Minha principal motivação para esta proposta é ter funções falsas com comportamento imutável.

Não necessariamente uma coisa ruim, IMHO, mas pode valer a pena considerar que, se muitas pessoas acharem que precisarão substituir todas as chamadas de API Sinon de qualquer maneira para sinon.fake(), elas também podem usar outra biblioteca (embora isso significaria que eles perderiam todo o conhecimento existente da API da Sinon).

Se as pessoas acharem que outra biblioteca atende melhor às suas necessidades, fico feliz por ajudá-las a aprender isso :)

Mas vamos manter os métodos spy e stub ou desativá-los, com algumas possíveis reduções de funcionalidade? Isso não estava claro para mim.

Mas vamos manter os métodos spy e stub ou desativá-los, com algumas possíveis reduções de funcionalidade?

Uma vez que fake pareça estável, então eu descontinuaria spy e stub e, em seguida, daria um ano para permitir que as pessoas tivessem tempo para atualizar.

Acho que devemos tentar o nosso melhor para fornecer codemods e ótima documentação, para ajudar as pessoas a mover seu código

Estou trabalhando em uma ramificação para as primeiras partes deste (sandbox padrão). Eu refatorei o código para que sandbox e collection agora sejam um. Eu tenho o sandbox padrão funcionando.

Vou organizar os commits nos próximos dias e, em seguida, criar uma ramificação neste repositório para a API atualizada.

Esta é uma ótima idéia, muito bem escrita btw.

Eu também adicionaria avisos de descontinuação a stubs e espiões.

Eu também estava pensando em talvez mudar a passagem de um objeto com chaves passando funções.

Isso adicionaria os seguintes benefícios:

  • Isso nos permitiria adicionar um type a essas funções para o usuário que deseja usar typescript ou outro tipo de verificadores estáticos
  • Os usuários obteriam erros ao tentar invocar funções para comportamentos que não existem
  • Poderíamos documentar essas funções separadamente e tornar os documentos ainda melhores
  • Poderíamos fornecer erros úteis ao passar argumentos que não fazem sentido para esses comportamentos e permitir que eles tenham argumentos opcionais/mais de um
  • Isso tornaria as coisas mais componíveis também (mesmo que eu não veja muitos casos para isso neste caso) e permitiria que as pessoas reutilizem comportamentos criados
  • IMO isso também seria mais simples do que ter um objeto com comportamento

Portanto, a API ficaria assim:

// It would be cool to allow users to import these using destructuring to make code more concise
import { resolves, rejects, returns } from 'sinon/behaviors'; 

var fake = sinon.fake(resolves('apple pie'))

var fake = sinon.fake(rejects(new TypeError('no more pie')));
var fake = sinon.fake(rejects('no more pie'));

var fake = sinon.fake(returns('apple pie'));

var fake = sinon.fake(throws(new RangeError('no more pie'));
var fake = sinon.fake(throws('no more pie'));

Quando se trata de implementar isso, pode ser apenas uma questão de retornar objetos muito simples como os que você está propondo. Então, se tivermos mais de um comportamento, podemos apenas mesclá-los.

Além disso, quando se trata de misturar coisas como onThirdCall e withArgs , acho que o que acontece nesses casos deve ser documentado.

Desculpe por revisar isso tão tarde. Os últimos meses foram muito corridos.

@lucasfcosta confira o PR #1586

Este problema foi marcado automaticamente como obsoleto porque não teve atividade recente. Será fechado se não ocorrer mais nenhuma atividade. Obrigado por suas contribuições.

A versão anterior 5.0.0 está causando problemas com as versões de pré-lançamento 5.0.0-next.* posteriores em package.json porque 5.0.0 é maior do que qualquer versão de pré-lançamento.

Como o 5.0.0 está disponível, acho que os números de pré-lançamento de next precisam ser aumentados talvez para 5.0.1-next.1 ?

Percebi isso porque outro pacote que eu estava usando estava recebendo uma msg obsoleta e seu package.json depende de "sinon": "^5.0.0-next.4"

npm WARN deprecated [email protected]: this version has been deprecated

Eu não tinha certeza se isso era digno de abrir uma nova edição para um problema de pré-lançamento, então um comentário aqui parecia mais seguro.

Outra solução seria lançar a próxima versão principal. O que você acha @sinonjs/core?

@mroderick Não posso mais dizer quais são todas as mudanças para a v5. Dos meus últimos testes funcionou bem e estou ansioso para usar as novas falsificações. É um novo major, então ei, envie 😄

Há apenas mais um PR #1764 que eu gostaria de fundir, antes de lançarmos a próxima versão principal.

Publiquei [email protected] , espero que isso facilite a vida das pessoas nesse meio tempo.

Obrigado, eu testei (sempre é bom verificar novamente) dependências em package.json e "sinon": "^5.0.1" dá um erro como deveria porque não há correspondência encontrada (nenhuma versão ainda) e "sinon": "^5.0.1-next.1" funciona corretamente obtendo essa versão.

Isso nunca foi grande coisa, só achei que valia a pena avisar, principalmente quando vi que a v5 estava em desenvolvimento há algum tempo, então não tinha certeza de quanto tempo até ser lançada. Acho que lançar em um futuro próximo parece uma boa ideia.

fake foi introduzido com #1768, que se tornou [email protected]

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

Questões relacionadas

fearphage picture fearphage  ·  3Comentários

brettz9 picture brettz9  ·  3Comentários

stevenmusumeche picture stevenmusumeche  ·  3Comentários

NathanHazout picture NathanHazout  ·  3Comentários

kevinburkeshyp picture kevinburkeshyp  ·  4Comentários