Skip to content

Make t.throws() accept async function as parameter #1650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ type AssertContext = {
deepEqual<U>(value: U, expected: U, message?: string): void;
// Assert that value is not deep equal to expected.
notDeepEqual<U>(value: U, expected: U, message?: string): void;
// Assert that function throws an error or promise rejects.
// Assert that the promise rejects, or the function throws or returns a rejected promise.
// @param error Can be a constructor, regex, error message or validation function.
throws: {
(value: PromiseLike<mixed>, error?: ErrorValidator, message?: string): Promise<any>;
(value: () => mixed, error?: ErrorValidator, message?: string): any;
};
// Assert that function doesn't throw an error or promise resolves.
// Assert that the promise resolves, or the function doesn't throw or return a resolved promise.
notThrows: {
(value: PromiseLike<mixed>, message?: string): Promise<void>;
(value: () => mixed, message?: string): void;
Expand Down
36 changes: 32 additions & 4 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,14 @@ function wrapAssertions(callbacks) {
coreAssertThrowsErrorArg = err;
}

let maybePromise;
const test = (fn, stack) => {
let actual;
let threw = false;
try {
coreAssert.throws(() => {
try {
fn();
maybePromise = fn();
} catch (err) {
actual = err;
threw = true;
Expand All @@ -224,7 +225,7 @@ function wrapAssertions(callbacks) {
}
};

if (promise) {
const handlePromise = promise => {
// Record stack before it gets lost in the promise chain.
const stack = getStack();
const intermediate = promise.then(value => {
Expand All @@ -238,13 +239,25 @@ function wrapAssertions(callbacks) {
pending(this, intermediate);
// Don't reject the returned promise, even if the assertion fails.
return intermediate.catch(noop);
};

if (promise) {
return handlePromise(promise);
}

try {
const retval = test(fn);
pass(this);
return retval;
} catch (err) {
if (maybePromise) {
if (isPromise(maybePromise)) {
return handlePromise(maybePromise);
}
if (isObservable(maybePromise)) {
return handlePromise(observableToPromise(maybePromise));
}
}
fail(this, err);
}
},
Expand All @@ -265,9 +278,12 @@ function wrapAssertions(callbacks) {
return;
}

let maybePromise;
const test = (fn, stack) => {
try {
coreAssert.doesNotThrow(fn);
coreAssert.doesNotThrow(() => {
maybePromise = fn();
});
} catch (err) {
throw new AssertionError({
assertion: 'notThrows',
Expand All @@ -278,17 +294,29 @@ function wrapAssertions(callbacks) {
}
};

if (promise) {
const handlePromise = promise => {
// Record stack before it gets lost in the promise chain.
const stack = getStack();
const intermediate = promise.then(noop, reason => test(makeRethrow(reason), stack));
pending(this, intermediate);
// Don't reject the returned promise, even if the assertion fails.
return intermediate.catch(noop);
};

if (promise) {
return handlePromise(promise);
}

try {
test(fn);
if (maybePromise) {
if (isPromise(maybePromise)) {
return handlePromise(maybePromise);
}
if (isObservable(maybePromise)) {
return handlePromise(observableToPromise(maybePromise));
}
}
pass(this);
} catch (err) {
fail(this, err);
Expand Down
16 changes: 14 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ Assert that `value` is not deeply equal to `expected`. The inverse of `.deepEqua

### `.throws(function|promise, [error, [message]])`

Assert that `function` throws an error, or `promise` rejects with an error.
Assert that `function` throws an error, `promise` rejects with an error, or `function` returns a rejected `promise`.

`error` can be an error constructor, error message, regex matched against the error message, or validation function.

Expand Down Expand Up @@ -952,9 +952,21 @@ test('rejects', async t => {
});
```

When testing an asynchronous function you must also wait for the assertion to complete:

```js
test('throws', async t => {
const error = await t.throws(async () => {
throw new TypeError('🦄');
}, TypeError);

t.is(error.message, '🦄');
});
```

### `.notThrows(function|promise, [message])`

Assert that `function` does not throw an error or that `promise` does not reject with an error.
Assert that `function` does not throw an error, `promise` does not reject with an error, or `function` returns a promise that does not reject with an error.

Like the `.throws()` assertion, when testing a promise you must wait for the assertion to complete:

Expand Down
39 changes: 39 additions & 0 deletions test/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const stripAnsi = require('strip-ansi');
const React = require('react');
const renderer = require('react-test-renderer');
const test = require('tap').test;
const Observable = require('zen-observable');
const assert = require('../lib/assert');
const snapshotManager = require('../lib/snapshot-manager');
const Test = require('../lib/test');
Expand Down Expand Up @@ -678,6 +679,28 @@ test('.throws() returns the rejection reason of promise', t => {
});
});

test('.throws() returns the rejection reason of a promise returned by the function', t => {
const expected = new Error();

return assertions.throws(() => {
return Promise.reject(expected);
}).then(actual => {
t.is(actual, expected);
t.end();
});
});

test('.throws() returns the error of an observable returned by the function', t => {
const expected = new Error();

return assertions.throws(() => {
return new Observable(observer => observer.error(expected));
}).then(actual => {
t.is(actual, expected);
t.end();
});
});

test('.throws() fails if passed a bad value', t => {
failsWith(t, () => {
assertions.throws('not a function');
Expand Down Expand Up @@ -732,6 +755,22 @@ test('.notThrows() returns undefined for a fulfilled promise', t => {
});
});

test('.notThrows() returns undefined for a fulfilled promise returned by the function', t => {
return assertions.notThrows(() => {
return Promise.resolve(Symbol(''));
}).then(actual => {
t.is(actual, undefined);
});
});

test('.notThrows() returns undefined for an observable returned by the function', t => {
return assertions.notThrows(() => {
return Observable.of(Symbol(''));
}).then(actual => {
t.is(actual, undefined);
});
});

test('.notThrows() fails if passed a bad value', t => {
failsWith(t, () => {
assertions.notThrows('not a function');
Expand Down
58 changes: 56 additions & 2 deletions test/observable.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ test('returning an observable from a legacy async fn is an error', t => {
t.end();
});

test('handle throws with thrown observable', t => {
test('handle throws with erroring observable', t => {
let result;
ava(a => {
a.plan(1);
Expand All @@ -67,7 +67,26 @@ test('handle throws with thrown observable', t => {
});
});

test('handle throws with long running thrown observable', t => {
test('handle throws with erroring observable returned by function', t => {
let result;
ava(a => {
a.plan(1);

const observable = new Observable(observer => {
observer.error(new Error());
});

return a.throws(() => observable);
}, r => {
result = r;
}).run().then(passed => {
t.is(passed, true);
t.is(result.result.assertCount, 1);
t.end();
});
});

test('handle throws with long running erroring observable', t => {
let result;
ava(a => {
a.plan(1);
Expand Down Expand Up @@ -104,6 +123,22 @@ test('handle throws with completed observable', t => {
});
});

test('handle throws with completed observable returned by function', t => {
let result;
ava(a => {
a.plan(1);

const observable = Observable.of();
return a.throws(() => observable);
}, r => {
result = r;
}).run().then(passed => {
t.is(passed, false);
t.is(result.reason.name, 'AssertionError');
t.end();
});
});

test('handle throws with regex', t => {
let result;
ava(a => {
Expand Down Expand Up @@ -196,3 +231,22 @@ test('handle notThrows with thrown observable', t => {
t.end();
});
});

test('handle notThrows with erroring observable returned by function', t => {
let result;
ava(a => {
a.plan(1);

const observable = new Observable(observer => {
observer.error(new Error());
});

return a.notThrows(() => observable);
}, r => {
result = r;
}).run().then(passed => {
t.is(passed, false);
t.is(result.reason.name, 'AssertionError');
t.end();
});
});
64 changes: 64 additions & 0 deletions test/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ test('handle throws with rejected promise', t => {
});
});

test('handle throws with rejected promise returned by function', t => {
let result;
ava(a => {
a.plan(1);

const promise = Promise.reject(new Error());
return a.throws(() => promise);
}, r => {
result = r;
}).run().then(passed => {
t.is(passed, true);
t.is(result.result.assertCount, 1);
t.end();
});
});

// TODO(team): This is a very slow test, and I can't figure out why we need it - James
test('handle throws with long running rejected promise', t => {
let result;
Expand Down Expand Up @@ -189,6 +205,22 @@ test('handle throws with resolved promise', t => {
});
});

test('handle throws with resolved promise returned by function', t => {
let result;
ava(a => {
a.plan(1);

const promise = Promise.resolve();
return a.throws(() => promise);
}, r => {
result = r;
}).run().then(passed => {
t.is(passed, false);
t.is(result.reason.name, 'AssertionError');
t.end();
});
});

test('handle throws with regex', t => {
let result;
ava(a => {
Expand Down Expand Up @@ -317,6 +349,38 @@ test('handle notThrows with rejected promise', t => {
});
});

test('handle notThrows with resolved promise returned by function', t => {
let result;
ava(a => {
a.plan(1);

const promise = Promise.resolve();
return a.notThrows(() => promise);
}, r => {
result = r;
}).run().then(passed => {
t.is(passed, true);
t.is(result.result.assertCount, 1);
t.end();
});
});

test('handle notThrows with rejected promise returned by function', t => {
let result;
ava(a => {
a.plan(1);

const promise = Promise.reject(new Error());
return a.notThrows(() => promise);
}, r => {
result = r;
}).run().then(passed => {
t.is(passed, false);
t.is(result.reason.name, 'AssertionError');
t.end();
});
});

test('assert pass', t => {
let result;
ava(a => {
Expand Down
2 changes: 2 additions & 0 deletions types/base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ export interface AssertContext {
* @param error Can be a constructor, regex, error message or validation function.
*/
throws(value: PromiseLike<any>, error?: ErrorValidator, message?: string): Promise<any>;
throws(value: () => PromiseLike<any>, error?: ErrorValidator, message?: string): Promise<any>;
throws(value: () => void, error?: ErrorValidator, message?: string): any;
/**
* Assert that function doesn't throw an error or promise resolves.
*/
notThrows(value: PromiseLike<any>, message?: string): Promise<void>;
notThrows(value: () => PromiseLike<any>, message?: string): Promise<void>;
notThrows(value: () => void, message?: string): void;
/**
* Assert that contents matches regex.
Expand Down