Skip to content

Commit d467e72

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 39e07ea commit d467e72

File tree

5 files changed

+198
-16
lines changed

5 files changed

+198
-16
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+
is?: Error;
9+
message?: string | RegExp;
10+
name?: string;
11+
of?: Constructor;
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+
is?: Error;
14+
message?: string | RegExp;
15+
name?: string;
16+
of?: Constructor;
17+
};
18+
19+
export type ThrowsErrorValidator = Constructor | RegExp | string | ThrowsExpectation;
1120

1221
export interface SnapshotOptions {
1322
id?: string;

lib/assert.js

+74-5
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',
@@ -172,16 +174,59 @@ function wrapAssertions(callbacks) {
172174
expected = {of: 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)) {
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, 'of') && typeof expected.of !== 'function') {
188+
fail(this, new AssertionError({
189+
assertion: 'throws',
190+
message: 'The `of` 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 'is':
217+
case 'message':
218+
case 'name':
219+
case 'of':
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,6 +241,18 @@ function wrapAssertions(callbacks) {
196241
});
197242
}
198243

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+
199256
if (expected.of && !(actual instanceof expected.of)) {
200257
throw new AssertionError({
201258
assertion: 'throws',
@@ -208,6 +265,18 @@ function wrapAssertions(callbacks) {
208265
});
209266
}
210267

268+
if (typeof expected.name === 'string' && actual.name !== expected.name) {
269+
throw new AssertionError({
270+
assertion: 'throws',
271+
message,
272+
stack,
273+
values: [
274+
formatWithLabel(`${prefix} unexpected exception:`, actual),
275+
formatWithLabel('Expected name to equal:', expected.name)
276+
]
277+
});
278+
}
279+
211280
if (typeof expected.message === 'string' && actual.message !== expected.message) {
212281
throw new AssertionError({
213282
assertion: 'throws',

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. 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. 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+
* `is`: the thrown error must be strictly equal to `expected.is`
922+
* `message`: either a string, which is compared against the thrown error's message, or a regular expression, which is matched against this message
923+
* `name`: the expected `.name` value of the thrown error
924+
* `of`: a constructor, the thrown error must be an instance of
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` or the empty object `{}`.
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+
}, {of: TypeError, message: '🦄'});
963968
});
964969
```
965970

test/assert.js

+94-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,50 @@ 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(() => {}, {of: null});
978+
}, {
979+
assertion: 'throws',
980+
message: 'The `of` property of the second argument to `t.throws()` must be a function',
981+
values: [{label: 'Called with:', formatted: /of: null/}]
982+
});
983+
984+
failsWith(t, () => {
985+
assertions.throws(() => {}, {message: null});
986+
}, {
987+
assertion: 'throws',
988+
message: 'The `message` property of the second argument to `t.throws()` must be a string or regular expression',
989+
values: [{label: 'Called with:', formatted: /message: null/}]
990+
});
991+
992+
failsWith(t, () => {
993+
assertions.throws(() => {}, {name: null});
994+
}, {
995+
assertion: 'throws',
996+
message: 'The `name` property of the second argument to `t.throws()` must be a string',
997+
values: [{label: 'Called with:', formatted: /name: null/}]
998+
});
999+
1000+
failsWith(t, () => {
1001+
assertions.throws(() => {}, {is: {}, message: '', name: '', of() {}, foo: null});
1002+
}, {
1003+
assertion: 'throws',
1004+
message: 'The second argument to `t.throws()` contains unexpected properties',
1005+
values: [{label: 'Called with:', formatted: /foo: null/}]
1006+
});
1007+
9181008
t.end();
9191009
});
9201010

0 commit comments

Comments
 (0)