Skip to content

Commit 94ac094

Browse files
committed
Support expectation object in t.throws()
Fixes #1047. Fixes #1676. A combination of the following expectations is supported: ```js t.throws(fn, {of: SyntaxError}) // err instanceof SyntaxError t.throws(fn, {name: 'SyntaxError'}) // err.name === 'SyntaxError' t.throws(fn, {is: expectedErrorInstance}) // err === expectedErrorInstance t.throws(fn, {message: 'expected error message'}) // err.message === 'expected error message' t.throws(fn, {message: /expected error message/}) // /expected error message/.test(err.message) ```
1 parent d3b63e7 commit 94ac094

File tree

5 files changed

+209
-19
lines changed

5 files changed

+209
-19
lines changed

index.d.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@ export interface ObservableLike {
22
subscribe(observer: (value: any) => void): void;
33
}
44

5-
export type ThrowsErrorValidator = (new (...args: Array<any>) => any) | RegExp | string;
5+
export type Constructor = (new (...args: Array<any>) => any);
6+
7+
export type ThrowsExpectation = {
8+
instanceOf?: Constructor;
9+
is?: Error;
10+
message?: string | RegExp;
11+
name?: string;
12+
};
13+
14+
export type ThrowsErrorValidator = Constructor | RegExp | string | ThrowsExpectation;
615

716
export interface SnapshotOptions {
817
id?: string;

index.js.flow

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ export interface ObservableLike {
77
subscribe(observer: (value: any) => void): void;
88
}
99

10-
export type ThrowsErrorValidator = Class<{constructor(...args: Array<any>): any}> | RegExp | string;
10+
export type Constructor = Class<{constructor(...args: Array<any>): any}>;
11+
12+
export type ThrowsExpectation = {
13+
instanceOf?: Constructor;
14+
is?: Error;
15+
message?: string | RegExp;
16+
name?: string;
17+
};
18+
19+
export type ThrowsErrorValidator = Constructor | RegExp | string | ThrowsExpectation;
1120

1221
export interface SnapshotOptions {
1322
id?: string;

lib/assert.js

+77-8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ function formatWithLabel(label, value) {
2828
return formatDescriptorWithLabel(label, concordance.describe(value, concordanceOptions));
2929
}
3030

31+
const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
32+
3133
class AssertionError extends Error {
3234
constructor(opts) {
3335
super(opts.message || '');
@@ -157,7 +159,7 @@ function wrapAssertions(callbacks) {
157159
}
158160
},
159161

160-
throws(thrower, expected, message) {
162+
throws(thrower, expected, message) { // eslint-disable-line complexity
161163
if (typeof thrower !== 'function' && !isPromise(thrower) && !isObservable(thrower)) {
162164
fail(this, new AssertionError({
163165
assertion: 'throws',
@@ -169,19 +171,62 @@ function wrapAssertions(callbacks) {
169171
}
170172

171173
if (typeof expected === 'function') {
172-
expected = {of: expected};
174+
expected = {instanceOf: expected};
173175
} else if (typeof expected === 'string' || expected instanceof RegExp) {
174176
expected = {message: expected};
175-
} else if (arguments.length === 1) {
177+
} else if (arguments.length === 1 || expected === null) {
176178
expected = {};
177-
} else if (expected !== null) {
179+
} else if (typeof expected !== 'object' || Array.isArray(expected) || Object.keys(expected).length === 0) {
178180
fail(this, new AssertionError({
179181
assertion: 'throws',
180-
improperUsage: true,
181-
message: 'The second argument to `t.throws()` must be a function, string, regular expression or `null`',
182+
message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`',
182183
values: [formatWithLabel('Called with:', expected)]
183184
}));
184185
return;
186+
} else {
187+
if (hasOwnProperty(expected, 'instanceOf') && typeof expected.instanceOf !== 'function') {
188+
fail(this, new AssertionError({
189+
assertion: 'throws',
190+
message: 'The `instanceOf` property of the second argument to `t.throws()` must be a function',
191+
values: [formatWithLabel('Called with:', expected)]
192+
}));
193+
return;
194+
}
195+
196+
if (hasOwnProperty(expected, 'message') && typeof expected.message !== 'string' && !(expected.message instanceof RegExp)) {
197+
fail(this, new AssertionError({
198+
assertion: 'throws',
199+
message: 'The `message` property of the second argument to `t.throws()` must be a string or regular expression',
200+
values: [formatWithLabel('Called with:', expected)]
201+
}));
202+
return;
203+
}
204+
205+
if (hasOwnProperty(expected, 'name') && typeof expected.name !== 'string') {
206+
fail(this, new AssertionError({
207+
assertion: 'throws',
208+
message: 'The `name` property of the second argument to `t.throws()` must be a string',
209+
values: [formatWithLabel('Called with:', expected)]
210+
}));
211+
return;
212+
}
213+
214+
for (const key of Object.keys(expected)) {
215+
switch (key) {
216+
case 'instanceOf':
217+
case 'is':
218+
case 'message':
219+
case 'name':
220+
continue;
221+
default:
222+
fail(this, new AssertionError({
223+
assertion: 'throws',
224+
message: 'The second argument to `t.throws()` contains unexpected properties',
225+
values: [formatWithLabel('Called with:', expected)]
226+
}));
227+
return;
228+
}
229+
}
185230
}
186231

187232
// Note: this function *must* throw exceptions, since it can be used
@@ -196,14 +241,38 @@ function wrapAssertions(callbacks) {
196241
});
197242
}
198243

199-
if (expected.of && !(actual instanceof expected.of)) {
244+
if (hasOwnProperty(expected, 'is') && actual !== expected.is) {
245+
throw new AssertionError({
246+
assertion: 'throws',
247+
message,
248+
stack,
249+
values: [
250+
formatWithLabel(`${prefix} unexpected exception:`, actual),
251+
formatWithLabel('Expected to be strictly equal to:', expected.is)
252+
]
253+
});
254+
}
255+
256+
if (expected.instanceOf && !(actual instanceof expected.instanceOf)) {
257+
throw new AssertionError({
258+
assertion: 'throws',
259+
message,
260+
stack,
261+
values: [
262+
formatWithLabel(`${prefix} unexpected exception:`, actual),
263+
formatWithLabel('Expected instance of:', expected.instanceOf)
264+
]
265+
});
266+
}
267+
268+
if (typeof expected.name === 'string' && actual.name !== expected.name) {
200269
throw new AssertionError({
201270
assertion: 'throws',
202271
message,
203272
stack,
204273
values: [
205274
formatWithLabel(`${prefix} unexpected exception:`, actual),
206-
formatWithLabel('Expected instance of:', expected.of)
275+
formatWithLabel('Expected name to equal:', expected.name)
207276
]
208277
});
209278
}

readme.md

+10-5
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,14 @@ Assert that an error is thrown. `thrower` can be a function which should throw,
916916

917917
The thrown value *must* be an error. It is returned so you can run more assertions against it.
918918

919-
`expected` can be a constructor, in which case the thrown value must be an instance of the constructor. It can be a string, which is compared against the thrown error's message, or a regular expression which is matched against this message. `expected` does not need to be specified. If you don't need it but do want to set an assertion message you have to specify `null`.
919+
`expected` can be a constructor, in which case the thrown error must be an instance of the constructor. It can be a string, which is compared against the thrown error's message, or a regular expression which is matched against this message. You can also specify a matcher object with one or more of the following properties:
920+
921+
* `instanceOf`: a constructor, the thrown error must be an instance of
922+
* `is`: the thrown error must be strictly equal to `expected.is`
923+
* `message`: either a string, which is compared against the thrown error's message, or a regular expression, which is matched against this message
924+
* `name`: the expected `.name` value of the thrown error
925+
926+
`expected` does not need to be specified. If you don't need it but do want to set an assertion message you have to specify `null`.
920927

921928
Example:
922929

@@ -955,11 +962,9 @@ When testing an asynchronous function you must also wait for the assertion to co
955962

956963
```js
957964
test('throws', async t => {
958-
const error = await t.throws(async () => {
965+
await t.throws(async () => {
959966
throw new TypeError('🦄');
960-
}, TypeError);
961-
962-
t.is(error.message, '🦄');
967+
}, {instanceOf: TypeError, message: '🦄'});
963968
});
964969
```
965970

test/assert.js

+102-4
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,24 @@ function eventuallyFails(t, fn) {
145145

146146
function passes(t, fn) {
147147
lastPassed = false;
148+
lastFailure = null;
148149
fn();
149150
if (lastPassed) {
150151
t.pass();
151152
} else {
152-
t.fail('Expected assertion to pass');
153+
t.ifError(lastFailure, 'Expected assertion to pass');
153154
}
154155
}
155156

156157
function eventuallyPasses(t, fn) {
157158
return add(() => {
158159
lastPassed = false;
160+
lastFailure = null;
159161
return fn().then(() => {
160162
if (lastPassed) {
161163
t.pass();
162164
} else {
163-
t.fail('Expected assertion to pass');
165+
t.ifError(lastFailure, 'Expected assertion to pass');
164166
}
165167
});
166168
});
@@ -726,8 +728,8 @@ test('.throws()', gather(t => {
726728
});
727729

728730
// Fails because thrown error's message is not equal to 'bar'
729-
const err = new Error('foo');
730731
failsWith(t, () => {
732+
const err = new Error('foo');
731733
assertions.throws(() => {
732734
throw err;
733735
}, 'bar');
@@ -742,6 +744,7 @@ test('.throws()', gather(t => {
742744

743745
// Fails because thrown error is not the right instance
744746
failsWith(t, () => {
747+
const err = new Error('foo');
745748
assertions.throws(() => {
746749
throw err;
747750
}, class Foo {});
@@ -756,6 +759,7 @@ test('.throws()', gather(t => {
756759

757760
// Passes because thrown error's message is equal to 'bar'
758761
passes(t, () => {
762+
const err = new Error('foo');
759763
assertions.throws(() => {
760764
throw err;
761765
}, 'foo');
@@ -768,6 +772,52 @@ test('.throws()', gather(t => {
768772
});
769773
});
770774

775+
// Passes because the correct error is thrown.
776+
passes(t, () => {
777+
const err = new Error('foo');
778+
assertions.throws(() => {
779+
throw err;
780+
}, {is: err});
781+
});
782+
783+
// Fails because the thrown value is not an error
784+
fails(t, () => {
785+
const obj = {};
786+
assertions.throws(() => {
787+
throw obj;
788+
}, {is: obj});
789+
});
790+
791+
// Fails because the thrown value is not the right one
792+
fails(t, () => {
793+
const err = new Error('foo');
794+
assertions.throws(() => {
795+
throw err;
796+
}, {is: {}});
797+
});
798+
799+
// Passes because the correct error is thrown.
800+
passes(t, () => {
801+
assertions.throws(() => {
802+
throw new TypeError();
803+
}, {name: 'TypeError'});
804+
});
805+
806+
// Fails because the thrown value is not an error
807+
fails(t, () => {
808+
assertions.throws(() => {
809+
const err = {name: 'Bob'};
810+
throw err;
811+
}, {name: 'Bob'});
812+
});
813+
814+
// Fails because the thrown value is not the right one
815+
fails(t, () => {
816+
assertions.throws(() => {
817+
throw new Error('foo');
818+
}, {name: 'TypeError'});
819+
});
820+
771821
// Fails because the promise is resolved, not rejected.
772822
eventuallyFailsWith(t, () => assertions.throws(Promise.resolve('foo')), {
773823
assertion: 'throws',
@@ -911,10 +961,58 @@ test('.throws() fails if passed a bad expectation', t => {
911961
assertions.throws(() => {}, true);
912962
}, {
913963
assertion: 'throws',
914-
message: 'The second argument to `t.throws()` must be a function, string, regular expression or `null`',
964+
message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`',
915965
values: [{label: 'Called with:', formatted: /true/}]
916966
});
917967

968+
failsWith(t, () => {
969+
assertions.throws(() => {}, {});
970+
}, {
971+
assertion: 'throws',
972+
message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`',
973+
values: [{label: 'Called with:', formatted: /\{\}/}]
974+
});
975+
976+
failsWith(t, () => {
977+
assertions.throws(() => {}, []);
978+
}, {
979+
assertion: 'throws',
980+
message: 'The second argument to `t.throws()` must be a function, string, regular expression, expectation object or `null`',
981+
values: [{label: 'Called with:', formatted: /\[\]/}]
982+
});
983+
984+
failsWith(t, () => {
985+
assertions.throws(() => {}, {instanceOf: null});
986+
}, {
987+
assertion: 'throws',
988+
message: 'The `instanceOf` property of the second argument to `t.throws()` must be a function',
989+
values: [{label: 'Called with:', formatted: /instanceOf: null/}]
990+
});
991+
992+
failsWith(t, () => {
993+
assertions.throws(() => {}, {message: null});
994+
}, {
995+
assertion: 'throws',
996+
message: 'The `message` property of the second argument to `t.throws()` must be a string or regular expression',
997+
values: [{label: 'Called with:', formatted: /message: null/}]
998+
});
999+
1000+
failsWith(t, () => {
1001+
assertions.throws(() => {}, {name: null});
1002+
}, {
1003+
assertion: 'throws',
1004+
message: 'The `name` property of the second argument to `t.throws()` must be a string',
1005+
values: [{label: 'Called with:', formatted: /name: null/}]
1006+
});
1007+
1008+
failsWith(t, () => {
1009+
assertions.throws(() => {}, {is: {}, message: '', name: '', of() {}, foo: null});
1010+
}, {
1011+
assertion: 'throws',
1012+
message: 'The second argument to `t.throws()` contains unexpected properties',
1013+
values: [{label: 'Called with:', formatted: /foo: null/}]
1014+
});
1015+
9181016
t.end();
9191017
});
9201018

0 commit comments

Comments
 (0)