Skip to content

Commit

Permalink
Fail tests that finish without running assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
novemberborn committed Mar 23, 2017
1 parent 09b23e0 commit 3a4553c
Show file tree
Hide file tree
Showing 26 changed files with 195 additions and 72 deletions.
1 change: 1 addition & 0 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ exports.run = () => {

const api = new Api({
failFast: conf.failFast,
failWithoutAssertions: conf.failWithoutAssertions !== false,
serial: conf.serial,
require: arrify(conf.require),
cacheEnabled: conf.cache !== false,
Expand Down
1 change: 1 addition & 0 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Runner = require('./runner');

const opts = globals.options;
const runner = new Runner({
failWithoutAssertions: opts.failWithoutAssertions,
serial: opts.serial,
bail: opts.failFast,
match: opts.match
Expand Down
3 changes: 2 additions & 1 deletion lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class Runner extends EventEmitter {

this.results = [];
this.tests = new TestCollection({
bail: options.bail
bail: options.bail,
failWithoutAssertions: options.failWithoutAssertions
});
this.hasStarted = false;
this._serial = options.serial;
Expand Down
5 changes: 3 additions & 2 deletions lib/test-collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class TestCollection extends EventEmitter {
super();

this.bail = options.bail;
this.failWithoutAssertions = options.failWithoutAssertions;
this.hasExclusive = false;
this.testCount = 0;

Expand Down Expand Up @@ -124,14 +125,14 @@ class TestCollection extends EventEmitter {
context = null;
}

return new Test(hook.metadata, title, hook.fn, context, this._emitTestResult);
return new Test(hook.metadata, title, hook.fn, false, context, this._emitTestResult);
}
_buildTest(test, context) {
if (!context) {
context = null;
}

return new Test(test.metadata, test.title, test.fn, context, this._emitTestResult);
return new Test(test.metadata, test.title, test.fn, this.failWithoutAssertions, context, this._emitTestResult);
}
_buildTestWithHooks(test) {
if (test.metadata.skipped || test.metadata.todo) {
Expand Down
10 changes: 9 additions & 1 deletion lib/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ Object.defineProperty(ExecutionContext.prototype, 'context', {enumerable: true})
}

class Test {
constructor(metadata, title, fn, contextRef, onResult) { // eslint-disable-line max-params
constructor(metadata, title, fn, failWithoutAssertions, contextRef, onResult) { // eslint-disable-line max-params
this.metadata = metadata;
this.title = title;
this.fn = isGeneratorFn(fn) ? co.wrap(fn) : fn;
this.failWithoutAssertions = failWithoutAssertions;
this.contextRef = contextRef;
this.onResult = onResult;

Expand Down Expand Up @@ -197,6 +198,12 @@ class Test {
}
}

verifyAssertions() {
if (this.failWithoutAssertions && !this.assertError && !this.calledEnd && this.planCount === null && this.assertCount === 0) {
this.saveFirstError(new Error('Test finished without running any assertions'));
}
}

callFn() {
try {
return {
Expand Down Expand Up @@ -280,6 +287,7 @@ class Test {
finish() {
this.finishing = true;
this.verifyPlan();
this.verifyAssertions();

if (this.pendingAssertions.length === 0) {
return this.completeFinish();
Expand Down
5 changes: 5 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ All of the CLI options can be configured in the `ava` section of your `package.j
],
"concurrency": 5,
"failFast": true,
"failWithoutAssertions": false,
"tap": true,
"powerAssert": false,
"require": [
Expand Down Expand Up @@ -326,6 +327,8 @@ test(function name(t) {

Assertion plans ensure tests only pass when a specific number of assertions have been executed. They'll help you catch cases where tests exit too early. They'll also cause tests to fail if too many assertions are executed, which can be useful if you have assertions inside callbacks or loops.

If you do not specify an assertion plan, your test will still fail if no assertions are executed. Set the `failWithoutAssertions` option to `false` in AVA's [`package.json` configuration](#configuration) to disable this behavior.

Note that, unlike [`tap`](https://www.npmjs.com/package/tap) and [`tape`](https://www.npmjs.com/package/tape), AVA does *not* automatically end a test when the planned assertion count is reached.

These examples will result in a passed test:
Expand Down Expand Up @@ -673,6 +676,8 @@ You can use any assertion library instead of or in addition to the built-in one,

This won't give you as nice an experience as you'd get with the [built-in assertions](#assertions) though, and you won't be able to use the [assertion planning](#assertion-planning) ([see #25](https://github.com/avajs/ava/issues/25)).

You'll have to configure AVA to not fail tests if no assertions are executed, because AVA can't tell if custom assertions pass. Set the `failWithoutAssertions` option to `false` in AVA's [`package.json` configuration](#configuration).

```js
import assert from 'assert';

Expand Down
10 changes: 5 additions & 5 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ function generateTests(prefix, apiCreator) {
runStatus.on('error', data => {
t.match(data.message, /Thrown by source-map-fixtures/);
t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-file.js:11.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-file.js:12.*$/m);
});
});

Expand All @@ -394,7 +394,7 @@ function generateTests(prefix, apiCreator) {
runStatus.on('error', data => {
t.match(data.message, /Thrown by source-map-fixtures/);
t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-file-browser-env.js:14.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-file-browser-env.js:15.*$/m);
});
});

Expand All @@ -415,7 +415,7 @@ function generateTests(prefix, apiCreator) {
runStatus.on('error', data => {
t.match(data.message, /Thrown by source-map-fixtures/);
t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-file.js:11.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-file.js:12.*$/m);
});
});

Expand All @@ -436,7 +436,7 @@ function generateTests(prefix, apiCreator) {
runStatus.on('error', data => {
t.match(data.message, /Thrown by source-map-fixtures/);
t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-initial-input.js:7.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-initial-input.js:14.*$/m);
});
});

Expand All @@ -457,7 +457,7 @@ function generateTests(prefix, apiCreator) {
runStatus.on('error', data => {
t.match(data.message, /Thrown by source-map-fixtures/);
t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-initial-input.js:7.*$/m);
t.match(data.stack, /^.*?Immediate\b.*source-map-initial-input.js:14.*$/m);
});
});

Expand Down
7 changes: 7 additions & 0 deletions test/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,3 +418,10 @@ test('uncaught exceptions are raised for worker errors even if the error cannot
t.end();
});
});

test('tests without assertions do not fail if failWithoutAssertions option is set to false', t => {
execCli([], {dirname: 'fixture/pkg-conf/fail-without-assertions'}, err => {
t.ifError(err);
t.end();
});
});
6 changes: 4 additions & 2 deletions test/fixture/_generate_source-map-initial.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ const fixture = mapFile('throws').require();
// string.
test('throw an uncaught exception', t => {
setImmediate(run);
t.pass();
})
const run = () => fixture.run();
`, {
`.trim(), {
filename: 'source-map-initial-input.js',
sourceMaps: true
sourceMaps: true,
presets: ['@ava/stage-4']
});

fs.writeFileSync(
Expand Down
4 changes: 3 additions & 1 deletion test/fixture/hooks-passing.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ test.after(pass);
test.afterEach(pass);
test(pass);

function pass() {}
function pass(t) {
t.pass();
}
2 changes: 1 addition & 1 deletion test/fixture/long-running.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ test.cb('slow', t => {
setTimeout(t.end, 5000);
});

test('fast', () => {});
test('fast', t => t.pass());
5 changes: 5 additions & 0 deletions test/fixture/pkg-conf/fail-without-assertions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"ava": {
"failWithoutAssertions": false
}
}
3 changes: 3 additions & 0 deletions test/fixture/pkg-conf/fail-without-assertions/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import test from '../../../..'

test(() => {})
3 changes: 2 additions & 1 deletion test/fixture/source-map-file-browser-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ global.XMLHttpRequest = function () {};
// The uncaught exception is passed to the corresponding cli test. The line
// numbers from the 'throws' fixture (which uses a map file), as well as the
// line of the fixture.run() call, should match the source lines.
test('throw an uncaught exception', () => {
test('throw an uncaught exception', t => {
setImmediate(run);
t.pass();
});

const run = () => fixture.run();
3 changes: 2 additions & 1 deletion test/fixture/source-map-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ const test = require('../../');
// The uncaught exception is passed to the corresponding cli test. The line
// numbers from the 'throws' fixture (which uses a map file), as well as the
// line of the fixture.run() call, should match the source lines.
test('throw an uncaught exception', () => {
test('throw an uncaught exception', t => {
setImmediate(run);
t.pass();
});

const run = () => fixture.run();
20 changes: 12 additions & 8 deletions test/fixture/source-map-initial.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
'use strict';

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

var _sourceMapFixtures = require('source-map-fixtures');

var _ = require('../../');

var _2 = _interopRequireDefault(_);

var fixture = (0, _sourceMapFixtures.mapFile)('throws').require();
(0, _2['default'])('throw an uncaught exception', function (t) {
setImmediate(run);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

const fixture = (0, _sourceMapFixtures.mapFile)('throws').require();

// The uncaught exception is passed to the corresponding cli test. The line
// numbers from the 'throws' fixture (which uses a map file), as well as the
// line of the fixture.run() call, should match the source lines from this
// string.
(0, _2.default)('throw an uncaught exception', t => {
setImmediate(run);
t.pass();
});
var run = function run() {
return fixture.run();
};
const run = () => fixture.run();
//# sourceMappingURL=./source-map-initial.js.map
// Generated using node test/fixtures/_generate-source-map-initial.js
2 changes: 1 addition & 1 deletion test/fixture/source-map-initial.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion test/fixture/throw-anonymous-function.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import test from '../../';

test('throw an uncaught exception', () => {
test('throw an uncaught exception', t => {
setImmediate(() => {
throw () => {};
});
t.pass();
});
3 changes: 2 additions & 1 deletion test/fixture/throw-named-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import test from '../../';

function fooFn() {}

test('throw an uncaught exception', () => {
test('throw an uncaught exception', t => {
setImmediate(() => {
throw fooFn;
});
t.pass();
});
3 changes: 2 additions & 1 deletion test/fixture/uncaught-exception.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import test from '../../';

test('throw an uncaught exception', () => {
test('throw an uncaught exception', t => {
setImmediate(() => {
throw new Error(`Can't catch me!`);
});
t.pass();
});
Loading

0 comments on commit 3a4553c

Please sign in to comment.