336

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.
Yves M.
  • 29,855
  • 23
  • 108
  • 144
doremi
  • 14,921
  • 30
  • 93
  • 148

7 Answers7

403

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.

Louis
  • 146,715
  • 28
  • 274
  • 320
  • I did pass a function didn't I? `model` instance has a function called get which I passed/called in expect. – doremi Feb 05 '14 at 20:00
  • Nope, see the explanation I've added while you were writing your comment. – Louis Feb 05 '14 at 20:02
  • 60
    Oof. Why don't the docs (http://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?) – ericsoco Aug 19 '14 at 16:44
  • When you pass some parameters that shouldn't throw, the test is still a pass though. – Alexandros Spyropoulos Nov 05 '14 at 00:08
  • @AlexandrosSpyropoulos It's not been my experience so please give an example of a case that illustrates what you mean. – Louis Nov 05 '14 at 00:09
  • 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. – Alexandros Spyropoulos Nov 05 '14 at 00:22
  • @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. – Louis Nov 05 '14 at 00:29
  • 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. – Alexandros Spyropoulos Nov 05 '14 at 01:21
  • If your test runner uses phantomjs, note that phantomjs v1 does not support "bind". Upgrade to v2. – jbarreiros Feb 05 '16 at 18:24
  • 11
    Note this won't (as of Sep 2017) work for async functions: see https://github.com/chaijs/chai/issues/882#issuecomment-322131680 and associated discussion. – ChrisV Sep 15 '17 at 14:43
217

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.');
Community
  • 1
  • 1
twiz
  • 9,041
  • 8
  • 52
  • 84
  • 8
    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. – Anand N Feb 15 '15 at 12:53
  • @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? – twiz Apr 18 '15 at 15:27
  • 3
    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(); }` – Anand N Apr 20 '15 at 07:58
  • 1
    Good solution except when you are using `this` inside the function to be called. Then `.bind` is the right way to go. – rabbitco Dec 01 '16 at 15:21
  • @AnandN Asynchronous function call does not **throw**, it **reject**s. For future reference, [chai-as-promised](https://www.npmjs.com/package/chai-as-promised) handles this quite nicely. – zypA13510 Mar 17 '20 at 03:28
113

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.');
danez
  • 1,659
  • 1
  • 12
  • 15
  • There CAN BE a problem with this because arrow functions take their surrounding scope for `this` – Eric Hodonsky Sep 11 '18 at 19:21
  • 3
    @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 `bind`ing functions to their `this` object manually. – Stijn de Witt Jan 30 '19 at 18:43
  • @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. – Eric Hodonsky Feb 04 '19 at 23:27
  • 1
    @Relic Yes I agree with you. It can be used to an advantage and can be a good reason to use an arrow function. – Stijn de Witt Feb 05 '19 at 21:30
  • Using arrow functions as opposed to an anonymous function helped me where I needed `this` properly scoped. Thanks! – Justin Hammond Oct 05 '22 at 19:23
97

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.

Charles Merriam
  • 19,908
  • 6
  • 73
  • 83
33

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
    
Michal Miky Jankovský
  • 3,089
  • 1
  • 35
  • 36
1

One other possible implementation, more cumbersome than the .bind() solution, but one that helps to make the point that expect() requires a function that provides a this context to the covered function, you can use a call(), e.g.,

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

SeanOlson
  • 379
  • 1
  • 9
0

I have found a nice way around it:

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

It's much more readable then my old version:

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
  • 2,289
  • 5
  • 25
  • 32