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. 7 Answers
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.
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.'); 5 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.'); 5 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 JavaScriptYou can check for specific errors using any assertion library:
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.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./) 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.
0examples 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
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('...');
I have found a nice way around it:
// The test, BDD style
it ("unsupported site", () => { The.function(myFunc) .with.arguments({url:""}) .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:""}; //Arrange function check_unsupported_site() { myFunc(args) } //Act check_unsupported_site.should.throw(/unsupported/) //Assert
});