Skip to content

Commit

Permalink
add max-asserts rule
Browse files Browse the repository at this point in the history
  • Loading branch information
jfmengels committed Feb 21, 2016
1 parent f1bca3d commit 604e7a7
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 10 deletions.
48 changes: 48 additions & 0 deletions docs/rules/max-asserts.md
@@ -0,0 +1,48 @@
# Limit the number of assertions in a test

Limit the amount of assertions in a test to enforce splitting up large tests into smaller ones.

Skipped assertions are counted.


## Fail

```js
/*eslint max-asserts: [2, 5]*/
const test = require('ava');

test('tests with too may asserts', (t) => {
const array = getArrayUpTo(6);

t.is(array.length, 6);
t.is(array[0], 1);
t.is(array[1], 2);
t.is(array[2], 3);
t.is(array[3], 4);
t.is(array[4], 5);
t.is(array[5], 6);
});
```


## Pass

```js
const test = require('ava');

test('my test name', (t) => {

test('tests with too may asserts', (t) => {
const array = getArrayUpTo(6);

t.same(array, [1, 2, 3, 4, 5, 6]);
});
```
## Options
The rule takes one option, a number, which is the maximum number of assertions for each test. The default is 5.
You can set the option in configuration like this:
"max-asserts": [2, 5]
6 changes: 4 additions & 2 deletions index.js
Expand Up @@ -2,16 +2,18 @@

module.exports = {
rules: {
'max-asserts': require('./rules/max-asserts'),
'no-cb-test': require('./rules/no-cb-test'),
'no-identical-title': require('./rules/no-identical-title')
'no-identical-title': require('./rules/no-identical-title'),
'no-only-test': require('./rules/no-only-test'),
'no-skip-test': require('./rules/no-skip-test'),
'test-ended': require('./rules/test-ended'),
'test-title': require('./rules/test-title')
},
rulesConfig: {
'max-asserts': [0, 5],
'no-cb-test': 0,
'no-identical-title': 0
'no-identical-title': 0,
'no-only-test': 0,
'no-skip-test': 0,
'test-ended': 0,
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -22,6 +22,7 @@ Configure it in package.json.
"ava"
],
"rules": {
"ava/max-asserts": [2, 5],
"ava/no-cb-test": 2,
"ava/no-identical-title": 2,
"ava/no-only-test": 2,
Expand All @@ -38,6 +39,7 @@ Configure it in package.json.

The rules will only activate in test files.

- [max-asserts](docs/rules/max-asserts) - Limit the number of assertions in a test.
- [no-cb-test](docs/rules/no-cb-test.md) - Ensure no `test.cb()` is used.
- [no-identical-title](docs/rules/no-identical-title.md) - Ensure no tests have the same title.
- [no-only-test](docs/rules/no-only-test.md) - Ensure no test.only() are present.
Expand Down
35 changes: 35 additions & 0 deletions rules/max-asserts.js
@@ -0,0 +1,35 @@
'use strict';
var util = require('../util');

module.exports = function (context) {
var maxAssertions = context.options[0] || 5;
var isTestFile = false;
var assertionCount = 0;

return {
CallExpression: function (node) {
if (util.isTestFile(node)) {
isTestFile = true;
return;
}

var callee = node.callee;
var name = callee.type === 'MemberExpression' ? callee.object.name : callee.name;
if (name === 'test') {
assertionCount = 0;
}
},

MemberExpression: function (node) {
if (isTestFile) {
// node.parent.type !== 'MemberExpression' --> don't count `t.skip.is(...)` twice
if (node.parent.type !== 'MemberExpression' && util.isAssertion(node)) {
assertionCount++;
if (assertionCount > maxAssertions) {
context.report(node, 'Too many assertions.');
}
}
}
}
};
};
2 changes: 0 additions & 2 deletions rules/no-identical-title.js
Expand Up @@ -2,9 +2,7 @@
var util = require('../util');

module.exports = function (context) {
var ifMultiple = context.options[0] === "if-multiple";
var isTestFile = false;
var testCount = 0;
var usedTitles = [];

return {
Expand Down
66 changes: 66 additions & 0 deletions test/max-asserts.js
@@ -0,0 +1,66 @@
import test from 'ava';
import {RuleTester} from 'eslint';
import rule from '../rules/max-asserts';

const ruleTester = new RuleTester({
env: {
es6: true
}
});

const errors = [{ruleId: 'max-asserts'}];
const header = `const test = require('ava');\n`;
function nbAssertions(n) {
return Array.from({length: n}).map(() => 't.is(1, 1);').join('\n');
}

test(() => {
ruleTester.run('max-asserts', rule, {
valid: [
`${header} test(function (t) { ${nbAssertions(3)} });`,
` ${header}
test(function (t) { ${nbAssertions(3)} });
test(function (t) { ${nbAssertions(3)} });
`,
`${header} test(function (t) { t.plan(5); ${nbAssertions(5)} });`,
`${header} test(function (t) { t.skip.is(1, 1); ${nbAssertions(4)} });`,
`${header} test.cb(function (t) { ${nbAssertions(5)} t.end(); });`,
{
code: `${header} test(function (t) { ${nbAssertions(3)} });`,
options: [3]
},
// shouldn't be triggered since it's not a test file
`test(function (t) { ${nbAssertions(10)} });`
],
invalid: [
{
code: `${header} test(function (t) { ${nbAssertions(6)} });`,
errors
},
{
code: ` ${header}
test(function (t) { ${nbAssertions(3)} });
test(function (t) { ${nbAssertions(6)} });
`,
errors
},
{
code: `${header} test(function (t) { t.plan(5); ${nbAssertions(6)} });`,
errors
},
{
code: `${header} test(function (t) { t.skip.is(1, 1); ${nbAssertions(5)} });`,
errors
},
{
code: `${header} test.cb(function (t) { ${nbAssertions(6)} t.end(); });`,
errors
},
{
code: `${header} test(function (t) { ${nbAssertions(4)} });`,
options: [3],
errors
}
]
});
});
38 changes: 32 additions & 6 deletions util.js
Expand Up @@ -24,6 +24,13 @@ exports.isTestType = function (callExp, type) {
callee.property.name === type;
};

function nameOfRootObject(node) {
if (node.object.type === 'MemberExpression') {
return nameOfRootObject(node.object);
}
return node.object.name;
}

/**
* Checks whether the given MemberExpression node has `test` as the root object.
* @param {ASTNode} node Node of type MemberExpression
Expand All @@ -34,9 +41,28 @@ exports.isTestType = function (callExp, type) {
* // => true
* @return {Boolean}
*/
exports.isCallFromTestObject = function isCallFromTestObject(node) {
if (node.object.type === 'MemberExpression') {
return isCallFromTestObject(node.object);
}
return node.object.name === 'test';
}
exports.isCallFromTestObject = function (node) {
return nameOfRootObject(node) === 'test';
};

/**
* Checks whether the given MemberExpression node is an assertion.
* Expects the assertion object to be `t`.
* Doesn't count `t.plan()` and `t.end()` as assertions.
* @param {ASTNode} node Node of type MemberExpression
* @example
* isAssertion(toASTNode('t.is(1, 1)'))
* // => true
* isAssertion(toASTNode('test.is(1, 1)'))
* // => false
* isAssertion(toASTNode('t.plan(5)'))
* // => false
* isAssertion(toASTNode('t.end()'))
* // => false
* @return {Boolean}
*/
exports.isAssertion = function (node) {
const notAssertionMethods = ['plan', 'end'];

return nameOfRootObject(node) === 't' && notAssertionMethods.indexOf(node.property.name) === -1;
};

0 comments on commit 604e7a7

Please sign in to comment.