Mocha / Chai expect.to.throw não capturando erros lançados

258

Estou tendo problemas para conseguir o Chai expect.to.throw funcione em um teste para meu aplicativo node.js. O teste continua falhando com o erro gerado, mas se eu envolver o caso de teste na tentativa de capturar e afirmar o erro detectado, ele funcionará.

Não expect.to.throwfunciona como eu acho que deveria ou algo assim?

it('should throw an error if you try to get an undefined property', function (done) {
  var params = { a: 'test', b: 'test', c: 'test' };
  var model = new TestModel(MOCK_REQUEST, params);

  // neither of these work
  expect(model.get('z')).to.throw('Property does not exist in model schema.');
  expect(model.get('z')).to.throw(new Error('Property does not exist in model schema.'));

  // this works
  try { 
    model.get('z'); 
  }
  catch(err) {
    expect(err).to.eql(new Error('Property does not exist in model schema.'));
  }

  done();
});

A falha:

19 passing (25ms)
  1 failing

  1) Model Base should throw an error if you try to get an undefined property:
     Error: Property does not exist in model schema.
do RE Mi
fonte

Respostas:

339

Você precisa passar uma função para expect. Como isso:

expect(model.get.bind(model, 'z')).to.throw('Property does not exist in model schema.');
expect(model.get.bind(model, 'z')).to.throw(new Error('Property does not exist in model schema.'));

Do jeito que você está fazendo isso, você está passando para expecto resultado da chamada model.get('z'). Mas, para testar se algo é lançado, você precisa passar uma função para expect, que expectse chamará. O bindmétodo usado acima cria uma nova função que, quando chamada, chamará model.getcom thiso valor de modele o primeiro argumento definido como 'z'.

Uma boa explicação bindpode ser encontrada aqui .

Louis
fonte
Eu passei em uma função, não foi? modelA instância tem uma função chamada get, que eu passei / chamei em expect.
doremi
Não, veja a explicação que adicionei enquanto você escrevia seu comentário.
Louis
47
Oof. Por que os documentos ( chaijs.com/api/bdd/#throw ) demonstram esse uso do bind? Parece que o cenário de teste mais comum to.throwé testar uma condição específica em uma função, o que requer chamar essa função com o estado / argumentos inválidos. (Por falar nisso .... por que não de chaijs.com deeplinks realmente deeplink?)
ericsoco
Quando você passa alguns parâmetros que não devem ser lançados, o teste ainda é um passe.
Alexandros Spyropoulos
6
Observe que isso (em setembro de 2017) não funcionará para funções assíncronas: consulte github.com/chaijs/chai/issues/882#issuecomment-322131680 e a discussão associada.
ChrisV
175

Como esta resposta diz , você também pode simplesmente agrupar seu código em uma função anônima como esta:

expect(function(){
    model.get('z');
}).to.throw('Property does not exist in model schema.');
twiz
fonte
7
Isso não está funcionando para chamadas de função assíncronas. Suponha que model.get seja assíncrono que retorna promessa. No entanto, gera um erro. Se eu tentar a abordagem acima, é "Tempo limite", pois precisamos notificar "concluído" para o mocha. Ao mesmo tempo, não posso tentar expect(function(){ model.get('z'); }).to.throw('Property does not exist in model schema.').notify(done); Como não há método de notificação.
Anand N
@ AnandN Se eu entendi o seu problema, parece que você só precisa refatorar o seu código para lidar com o erro. O erro não tratado na função assíncrona também não será um problema no seu aplicativo real?
Twiz
2
Obrigado twiz pela sua resposta. Estamos trabalhando em um ambiente integrado, o módulo using cuida de capturar as exceções. Portanto, o problema é quando tentamos executar casos de teste de unidade. Finalmente foi utilizada a abordagem abaixo para fazê-lo funcionar catch (err) { expect(err).equal('Error message to be checked'); done(); }
Anand N
1
Boa solução, exceto quando você estiver usando thisdentro da função a ser chamada. Então .bindé o caminho certo a seguir.
rabbitco
Chamada de função @AnandN Asynchronous não jogue , ele rejeitar s. Para referência futura, o chai-conforme-prometido lida com isso muito bem.
user5532169
85

E se você já estiver usando o ES6 / ES2015, também poderá usar a função de seta. É basicamente o mesmo que usar uma função anônima normal, mas mais curta.

expect(() => model.get('z')).to.throw('Property does not exist in model schema.');
Daniel T.
fonte
PODE EXISTIR um problema com isso, porque as funções de seta adotam o escopo circundante parathis
Eric Hodonsky
1
@ Relic Sim, muito verdade. Isso também pode ser uma grande vantagem das funções de seta. As funções de seta 'herdam' thisdo escopo em que foram criadas. Geralmente, isso pode ser uma vantagem, pois evita a necessidade de bindinserir funções em seu thisobjeto manualmente.
Stijn de Witt
@StijndeWitt, isso não é uma vantagem ou desvantagem, é um controle de escopo e intencional. Na verdade, é um açúcar de sintaxe para uso binde sempre vincula ao thisescopo pai. Minha intenção no comentário era apenas garantir que os leitores estivessem cientes de uma possível queda de poço.
Eric Hodonsky
1
@Relic Sim, eu concordo com você. Pode ser usado com vantagem e pode ser um bom motivo para usar uma função de seta.
Stijn de Witt
75

Essa pergunta possui muitas, muitas duplicatas, incluindo perguntas que não mencionam a biblioteca de asserções Chai. Aqui estão os conceitos básicos reunidos:

A asserção deve chamar a função, em vez de avaliar imediatamente.

assert.throws(x.y.z);      
   // FAIL.  x.y.z throws an exception, which immediately exits the
   // enclosing block, so assert.throw() not called.
assert.throws(()=>x.y.z);  
   // assert.throw() is called with a function, which only throws
   // when assert.throw executes the function.
assert.throws(function () { x.y.z });   
   // if you cannot use ES6 at work
function badReference() { x.y.z }; assert.throws(badReference);  
   // for the verbose
assert.throws(()=>model.get(z));  
   // the specific example given.
homegrownAssertThrows(model.get, z);
   //  a style common in Python, but not in JavaScript

Você pode verificar se há erros específicos usando qualquer biblioteca de asserções:

  assert.throws(() => x.y.z);
  assert.throws(() => x.y.z, ReferenceError);
  assert.throws(() => x.y.z, ReferenceError, /is not defined/);
  assert.throws(() => x.y.z, /is not defined/);
  assert.doesNotThrow(() => 42);
  assert.throws(() => x.y.z, Error);
  assert.throws(() => model.get.z, /Property does not exist in model schema./)

Devemos

  should.throws(() => x.y.z);
  should.throws(() => x.y.z, ReferenceError);
  should.throws(() => x.y.z, ReferenceError, /is not defined/);
  should.throws(() => x.y.z, /is not defined/);
  should.doesNotThrow(() => 42);
  should.throws(() => x.y.z, Error);
  should.throws(() => model.get.z, /Property does not exist in model schema./)

Chai Expect

  expect(() => x.y.z).to.throw();
  expect(() => x.y.z).to.throw(ReferenceError);
  expect(() => x.y.z).to.throw(ReferenceError, /is not defined/);
  expect(() => x.y.z).to.throw(/is not defined/);
  expect(() => 42).not.to.throw();
  expect(() => x.y.z).to.throw(Error);
  expect(() => model.get.z).to.throw(/Property does not exist in model schema./);

Você deve lidar com exceções que 'escapam' do teste

it('should handle escaped errors', function () {
  try {
    expect(() => x.y.z).not.to.throw(RangeError);
  } catch (err) {
    expect(err).to.be.a(ReferenceError);
  }
});

Isso pode parecer confuso no começo. Como andar de bicicleta, apenas 'clica' para sempre quando clica.

Charles Merriam
fonte
15

exemplos de doc ...;)

porque você confia no thiscontexto:

  • que é perdido quando a função é invocada por .throw
  • não há como saber o que isso deveria ser

você precisa usar uma destas opções:

  • agrupar a chamada de método ou função dentro de outra função
  • vincular o contexto

    // wrap the method or function call inside of another function
    expect(function () { cat.meow(); }).to.throw();  // Function expression
    expect(() => cat.meow()).to.throw();             // ES6 arrow function
    
    // bind the context
    expect(cat.meow.bind(cat)).to.throw();           // Bind
Michal Miky Jankovský
fonte
É assim que eu faço também. Eu acho, que a implementação ES6 é de longe o mais legível um
relief.melone
1

Outra implementação possível, mais complicada do que a solução .bind (), mas que ajuda a enfatizar que expect () requer uma função que fornece um thiscontexto para a função coberta, você pode usar um call(), por exemplo,

expect(function() {model.get.call(model, 'z');}).to.throw('...');

SeanOlson
fonte
0

Eu encontrei uma boa maneira de contornar isso:

// The test, BDD style
it ("unsupported site", () => {
    The.function(myFunc)
    .with.arguments({url:"https://www.ebay.com/"})
    .should.throw(/unsupported/);
});


// The function that does the magic: (lang:TypeScript)
export const The = {
    'function': (func:Function) => ({
        'with': ({
            'arguments': function (...args:any) {
                return () => func(...args);
            }
        })
    })
};

É muito mais legível que minha versão antiga:

it ("unsupported site", () => {
    const args = {url:"https://www.ebay.com/"}; //Arrange
    function check_unsupported_site() { myFunc(args) } //Act
    check_unsupported_site.should.throw(/unsupported/) //Assert
});
Dani-Br
fonte