Skip to content

Commit affbb45

Browse files
novemberbornsindresorhus
authored andcommitted
Fail tests that finish with pending assertions
`t.throws()` and `t.notThrows()` can be called with an observable or promise. This commit forces users to wait for the assertion to complete before finishing the test. Usually this means the test has to be written like: ```js test(async t => { await t.throws(Promise.reject(new Error())) }) ``` Or for callback tests: ```js test.cb(t => { t.throws(Promise.reject(new Error())).then(t.end) }) ``` This simplifies internal logic and helps discourage code like in #1327. Anecdotally users are surprised by the previous behavior where a synchronous test worked with an asynchronous assertion (#1327 (comment)). Fixes #1327.
1 parent 2e48970 commit affbb45

File tree

3 files changed

+98
-128
lines changed

3 files changed

+98
-128
lines changed

lib/test.js

+17-19
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class Test {
111111
this.finishDueToAttributedError = null;
112112
this.finishDueToInactivity = null;
113113
this.finishing = false;
114-
this.pendingAssertions = [];
114+
this.pendingAssertionCount = 0;
115115
this.pendingThrowsAssertion = null;
116116
this.planCount = null;
117117
this.startedAt = 0;
@@ -166,7 +166,10 @@ class Test {
166166
}
167167

168168
this.assertCount++;
169-
this.pendingAssertions.push(promise.catch(err => this.saveFirstError(err)));
169+
this.pendingAssertionCount++;
170+
promise
171+
.catch(err => this.saveFirstError(err))
172+
.then(() => this.pendingAssertionCount--);
170173
}
171174

172175
addFailedAssertion(error) {
@@ -208,8 +211,12 @@ class Test {
208211
}
209212

210213
verifyAssertions() {
211-
if (this.failWithoutAssertions && !this.assertError && !this.calledEnd && this.planCount === null && this.assertCount === 0) {
212-
this.saveFirstError(new Error('Test finished without running any assertions'));
214+
if (!this.assertError) {
215+
if (this.failWithoutAssertions && !this.calledEnd && this.planCount === null && this.assertCount === 0) {
216+
this.saveFirstError(new Error('Test finished without running any assertions'));
217+
} else if (this.pendingAssertionCount > 0) {
218+
this.saveFirstError(new Error('Test finished, but an assertion is still pending'));
219+
}
213220
}
214221
}
215222

@@ -375,21 +382,6 @@ class Test {
375382
this.verifyPlan();
376383
this.verifyAssertions();
377384

378-
if (this.assertError || this.pendingAssertions.length === 0) {
379-
return this.completeFinish();
380-
}
381-
382-
// Finish after potential errors from pending assertions have been consumed.
383-
return Promise.all(this.pendingAssertions).then(() => this.completeFinish());
384-
}
385-
386-
finishPromised() {
387-
return new Promise(resolve => {
388-
resolve(this.finish());
389-
});
390-
}
391-
392-
completeFinish() {
393385
this.duration = globals.now() - this.startedAt;
394386

395387
let reason = this.assertError;
@@ -413,6 +405,12 @@ class Test {
413405

414406
return passed;
415407
}
408+
409+
finishPromised() {
410+
return new Promise(resolve => {
411+
resolve(this.finish());
412+
});
413+
}
416414
}
417415

418416
module.exports = Test;

readme.md

+16
Original file line numberDiff line numberDiff line change
@@ -965,10 +965,26 @@ test('rejects', async t => {
965965
});
966966
```
967967

968+
When testing a promise you must wait for the assertion to complete:
969+
970+
```js
971+
test('rejects', async t => {
972+
await t.throws(promise);
973+
});
974+
```
975+
968976
### `.notThrows(function|promise, [message])`
969977

970978
Assert that `function` does not throw an error or that `promise` does not reject with an error.
971979

980+
Like the `.throws()` assertion, when testing a promise you must wait for the assertion to complete:
981+
982+
```js
983+
test('rejects', async t => {
984+
await t.notThrows(promise);
985+
});
986+
```
987+
972988
### `.regex(contents, regex, [message])`
973989

974990
Assert that `contents` matches `regex`.

test/test.js

+65-109
Original file line numberDiff line numberDiff line change
@@ -450,10 +450,12 @@ test('throws and notThrows work with promises', t => {
450450
let result;
451451
ava(a => {
452452
a.plan(2);
453-
a.throws(delay.reject(10, new Error('foo')), 'foo');
454-
a.notThrows(delay(20).then(() => {
455-
asyncCalled = true;
456-
}));
453+
return Promise.all([
454+
a.throws(delay.reject(10, new Error('foo')), 'foo'),
455+
a.notThrows(delay(20).then(() => {
456+
asyncCalled = true;
457+
}))
458+
]);
457459
}, null, r => {
458460
result = r;
459461
}).run().then(passed => {
@@ -495,143 +497,97 @@ test('cb test that throws sync', t => {
495497
t.end();
496498
});
497499

498-
test('waits for t.throws to resolve after t.end is called', t => {
500+
test('multiple resolving and rejecting promises passed to t.throws/t.notThrows', t => {
499501
let result;
500-
ava.cb(a => {
501-
a.plan(1);
502-
a.notThrows(delay(10), 'foo');
503-
a.end();
502+
ava(a => {
503+
a.plan(6);
504+
const promises = [];
505+
for (let i = 0; i < 3; i++) {
506+
promises.push(
507+
a.throws(delay.reject(10, new Error('foo')), 'foo'),
508+
a.notThrows(delay(10), 'foo')
509+
);
510+
}
511+
return Promise.all(promises);
504512
}, null, r => {
505513
result = r;
506514
}).run().then(passed => {
507515
t.is(passed, true);
508-
t.is(result.result.planCount, 1);
509-
t.is(result.result.assertCount, 1);
516+
t.is(result.result.planCount, 6);
517+
t.is(result.result.assertCount, 6);
510518
t.end();
511519
});
512520
});
513521

514-
test('waits for t.throws to reject after t.end is called', t => {
522+
test('fails if test ends while there are pending assertions', t => {
515523
let result;
516-
ava.cb(a => {
517-
a.plan(1);
518-
a.throws(delay.reject(10, new Error('foo')), 'foo');
519-
a.end();
524+
const passed = ava(a => {
525+
a.throws(Promise.reject(new Error()));
520526
}, null, r => {
521527
result = r;
522-
}).run().then(passed => {
523-
t.is(passed, true);
524-
t.is(result.result.planCount, 1);
525-
t.is(result.result.assertCount, 1);
526-
t.end();
527-
});
528-
});
528+
}).run();
529529

530-
test('waits for t.throws to resolve after the promise returned from the test resolves', t => {
531-
let result;
532-
ava(a => {
533-
a.plan(1);
534-
a.notThrows(delay(10), 'foo');
535-
return Promise.resolve();
536-
}, null, r => {
537-
result = r;
538-
}).run().then(passed => {
539-
t.is(passed, true);
540-
t.is(result.result.planCount, 1);
541-
t.is(result.result.assertCount, 1);
542-
t.end();
543-
});
530+
t.is(passed, false);
531+
t.is(result.reason.name, 'Error');
532+
t.match(result.reason.message, /Test finished, but an assertion is still pending/);
533+
t.end();
544534
});
545535

546-
test('waits for t.throws to reject after the promise returned from the test resolves', t => {
536+
test('fails if callback test ends while there are pending assertions', t => {
547537
let result;
548-
ava(a => {
549-
a.plan(1);
550-
a.throws(delay.reject(10, new Error('foo')), 'foo');
551-
return Promise.resolve();
538+
const passed = ava.cb(a => {
539+
a.throws(Promise.reject(new Error()));
540+
a.end();
552541
}, null, r => {
553542
result = r;
554-
}).run().then(passed => {
555-
t.is(passed, true);
556-
t.is(result.result.planCount, 1);
557-
t.is(result.result.assertCount, 1);
558-
t.end();
559-
});
560-
});
543+
}).run();
561544

562-
test('multiple resolving and rejecting promises passed to t.throws/t.notThrows', t => {
563-
let result;
564-
ava(a => {
565-
a.plan(6);
566-
for (let i = 0; i < 3; i++) {
567-
a.throws(delay.reject(10, new Error('foo')), 'foo');
568-
a.notThrows(delay(10), 'foo');
569-
}
570-
}, null, r => {
571-
result = r;
572-
}).run().then(passed => {
573-
t.is(passed, true);
574-
t.is(result.result.planCount, 6);
575-
t.is(result.result.assertCount, 6);
576-
t.end();
577-
});
545+
t.is(passed, false);
546+
t.is(result.reason.name, 'Error');
547+
t.match(result.reason.message, /Test finished, but an assertion is still pending/);
548+
t.end();
578549
});
579550

580-
test('number of assertions matches t.plan when the test exits, but before all pending assertions resolve another is added', t => {
551+
test('fails if async test ends while there are pending assertions', t => {
581552
let result;
582553
ava(a => {
583-
a.plan(2);
584-
a.throws(delay.reject(10, new Error('foo')), 'foo');
585-
a.notThrows(delay(10), 'foo');
586-
setTimeout(() => {
587-
a.pass();
588-
}, 5);
554+
a.throws(Promise.reject(new Error()));
555+
return Promise.resolve();
589556
}, null, r => {
590557
result = r;
591558
}).run().then(passed => {
592559
t.is(passed, false);
593-
t.match(result.reason.message, /Assertion passed, but test has already finished/);
594560
t.is(result.reason.name, 'Error');
561+
t.match(result.reason.message, /Test finished, but an assertion is still pending/);
595562
t.end();
596563
});
597564
});
598565

599-
test('number of assertions matches t.plan when the test exits, but before all pending assertions resolve, a failing assertion is added', t => {
600-
let result;
566+
// This behavior is incorrect, but feedback cannot be provided to the user due to
567+
// https://github.com/avajs/ava/issues/1330
568+
test('no crash when adding assertions after the test has ended', t => {
569+
t.plan(3);
570+
601571
ava(a => {
602-
a.plan(2);
603-
a.throws(delay.reject(10, new Error('foo')), 'foo');
604-
a.notThrows(delay(10), 'foo');
605-
setTimeout(() => {
606-
a.fail();
607-
}, 5);
608-
}, null, r => {
609-
result = r;
610-
}).run().then(passed => {
611-
t.is(passed, false);
612-
t.match(result.reason.message, /Assertion failed, but test has already finished/);
613-
t.is(result.reason.name, 'Error');
614-
t.end();
615-
});
616-
});
572+
a.pass();
573+
setImmediate(() => {
574+
t.doesNotThrow(() => a.pass());
575+
});
576+
}).run();
617577

618-
test('number of assertions doesn\'t match plan when the test exits, but before all promises resolve another is added', t => {
619-
let result;
620-
const passed = ava(a => {
621-
a.plan(3);
622-
a.throws(delay.reject(10, new Error('foo')), 'foo');
623-
a.notThrows(delay(10), 'foo');
624-
setTimeout(() => {
625-
a.throws(Promise.reject(new Error('foo')), 'foo');
626-
}, 5);
627-
}, null, r => {
628-
result = r;
578+
ava(a => {
579+
a.pass();
580+
setImmediate(() => {
581+
t.doesNotThrow(() => a.fail());
582+
});
629583
}).run();
630584

631-
t.is(passed, false);
632-
t.is(result.reason.assertion, 'plan');
633-
t.is(result.reason.operator, '===');
634-
t.end();
585+
ava(a => {
586+
a.pass();
587+
setImmediate(() => {
588+
t.doesNotThrow(() => a.notThrows(Promise.resolve()));
589+
});
590+
}).run();
635591
});
636592

637593
test('contextRef', t => {
@@ -725,8 +681,8 @@ test('failing tests must not return a fulfilled promise', t => {
725681
test('failing tests pass when returning a rejected promise', t => {
726682
ava.failing(a => {
727683
a.plan(1);
728-
a.notThrows(delay(10), 'foo');
729-
return Promise.reject();
684+
return a.notThrows(delay(10), 'foo')
685+
.then(() => Promise.reject());
730686
}).run().then(passed => {
731687
t.is(passed, true);
732688
t.end();
@@ -735,7 +691,7 @@ test('failing tests pass when returning a rejected promise', t => {
735691

736692
test('failing tests pass with `t.throws(nonThrowingPromise)`', t => {
737693
ava.failing(a => {
738-
a.throws(Promise.resolve(10));
694+
return a.throws(Promise.resolve(10));
739695
}).run().then(passed => {
740696
t.is(passed, true);
741697
t.end();
@@ -745,7 +701,7 @@ test('failing tests pass with `t.throws(nonThrowingPromise)`', t => {
745701
test('failing tests fail with `t.notThrows(throws)`', t => {
746702
let result;
747703
ava.failing(a => {
748-
a.notThrows(Promise.resolve('foo'));
704+
return a.notThrows(Promise.resolve('foo'));
749705
}, null, r => {
750706
result = r;
751707
}).run().then(passed => {

0 commit comments

Comments
 (0)