Mocha / Chai expect.to.throw not catching thrown errors

215,006

Solution 1

You have to pass a function to expect. Like this:

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.'));

The way you are doing it, you are passing to expect the result of calling model.get('z'). But to test whether something is thrown, you have to pass a function to expect, which expect will call itself. The bind method used above creates a new function which when called will call model.get with this set to the value of model and the first argument set to 'z'.

A good explanation of bind can be found here.

Solution 2

As this answer says, you can also just wrap your code in an anonymous function like this:

expect(function(){
    model.get('z');
}).to.throw('Property does not exist in model schema.');

Solution 3

And if you are already using ES6/ES2015 then you can also use an arrow function. It is basically the same as using a normal anonymous function but shorter.

expect(() => model.get('z')).to.throw('Property does not exist in model schema.');

Solution 4

This question has many, many duplicates, including questions not mentioning the Chai assertion library. Here are the basics collected together:

The assertion must call the function, instead of it evaluating immediately.

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

You can check for specific errors using any assertion library:

Node

  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./)

Should

  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./);

You must handle exceptions that 'escape' the test

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

This can look confusing at first. Like riding a bike, it just 'clicks' forever once it clicks.

Solution 5

examples from doc... ;)

because you rely on this context:

  • which is lost when the function is invoked by .throw
  • there’s no way for it to know what this is supposed to be

you have to use one of these options:

  • wrap the method or function call inside of another function
  • bind the context

    // 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
    
Share:
215,006

Related videos on Youtube

doremi
Author by

doremi

Updated on May 25, 2020

Comments

  • doremi
    doremi almost 4 years

    I'm having issues getting Chai's expect.to.throw to work in a test for my node.js app. The test keeps failing on the thrown error, but If I wrap the test case in try and catch and assert on the caught error, it works.

    Does expect.to.throw not work like I think it should or something?

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

    The failure:

    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.
    
  • doremi
    doremi over 10 years
    I did pass a function didn't I? model instance has a function called get which I passed/called in expect.
  • Louis
    Louis over 10 years
    Nope, see the explanation I've added while you were writing your comment.
  • ericsoco
    ericsoco over 9 years
    Oof. Why don't the docs (chaijs.com/api/bdd/#throw) demonstrate this usage of bind? Seems like the most common testing scenario for to.throw is testing a particular condition within a function, which requires calling that function with the invalid state/arguments. (For that matter....why don't chaijs.com's deeplinks actually deeplink?)
  • Alexandros Spyropoulos
    Alexandros Spyropoulos over 9 years
    When you pass some parameters that shouldn't throw, the test is still a pass though.
  • Louis
    Louis over 9 years
    @AlexandrosSpyropoulos It's not been my experience so please give an example of a case that illustrates what you mean.
  • Alexandros Spyropoulos
    Alexandros Spyropoulos over 9 years
    The first two filter pass, that means the the method runs as expected, the third case though (pass a valid parameter and do not throw an error) fails with... AssertionError: expected [Function] to not throw an error but 'RangeError: Value out of bounds!' was thrown That means that when the throw test executes the function, it always throws.
  • Louis
    Louis over 9 years
    @AlexandrosSpyropoulos You are misusing bind. See in my answer how I have model.get.bind(model, ... You need abs.set.bind(abs, ... You need to pass the object instance as the first argument. It is the first argument to bind that sets the value of this inside the function.
  • Alexandros Spyropoulos
    Alexandros Spyropoulos over 9 years
    Ok got it, thank you so much... so what it basically does, is that we pass a version of the abs.set with preset context.
  • Anand N
    Anand N over 9 years
    This is not working for asynchronous function calls. Suppose model.get is async that returns promise. However it throws an error. If I try the above approach, it is "Timing out" as we have to notify "done" to mocha. At the same time, I can't try expect(function(){ model.get('z'); }).to.throw('Property does not exist in model schema.').notify(done); As there is no notify method.
  • twiz
    twiz about 9 years
    @AnandN If I understand your problem, this sounds like you just need to refactor your code to handle the error. Wont the unhandled error in the async function be a problem in your actual app as well?
  • Anand N
    Anand N about 9 years
    Thanks twiz for your reply. We are working in an integrated environment, the using module takes care of catching the exceptions. So, the problem is when we try to run unit test cases. Finally we used the below approach to get it working catch (err) { expect(err).equal('Error message to be checked'); done(); }
  • jbarreiros
    jbarreiros over 8 years
    If your test runner uses phantomjs, note that phantomjs v1 does not support "bind". Upgrade to v2.
  • rabbitco
    rabbitco over 7 years
    Good solution except when you are using this inside the function to be called. Then .bind is the right way to go.
  • ChrisV
    ChrisV over 6 years
    Note this won't (as of Sep 2017) work for async functions: see github.com/chaijs/chai/issues/882#issuecomment-322131680 and associated discussion.
  • Eric Hodonsky
    Eric Hodonsky over 5 years
    There CAN BE a problem with this because arrow functions take their surrounding scope for this
  • Geek Stocks
    Geek Stocks over 5 years
    October 2018: still a great answer!
  • Stijn de Witt
    Stijn de Witt over 5 years
    @Relic Yes, very true. This can also be a big advantage of arrow functions. Arrow functions 'inherit' this from the scope they where created in. Often this can be an advantage, as it avoid the need for binding functions to their this object manually.
  • Eric Hodonsky
    Eric Hodonsky over 5 years
    @StijndeWitt this is not an advantage or disadvantage, it's scope control and intentional. It's actually syntax sugar for using bind and always bind's to this of the parent scope. My intention in the comment was only to ensure readers were aware of a potential pit fall.
  • Stijn de Witt
    Stijn de Witt over 5 years
    @Relic Yes I agree with you. It can be used to an advantage and can be a good reason to use an arrow function.
  • il0v3d0g
    il0v3d0g almost 5 years
    Thank you @ChrisV for your comment! I was able to solve my case by reading your comment and going to the link!
  • relief.melone
    relief.melone almost 5 years
    This is how I do it as well. I find, that the ES6 implementation is by far the most readable one
  • zypA13510
    zypA13510 about 4 years
    @AnandN Asynchronous function call does not throw, it rejects. For future reference, chai-as-promised handles this quite nicely.