Skip to content
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

feat: Add 'no-empty-tests' rule #99

Closed
wants to merge 5 commits into from
Closed
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: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ You can add rules:
"cypress/assertion-before-screenshot": "warn",
"cypress/no-force": "warn",
"cypress/no-async-tests": "error",
"cypress/no-pause": "error"
"cypress/no-pause": "error",
"cypress/no-empty-tests": "error"
}
}
```
Expand Down Expand Up @@ -121,6 +122,7 @@ Rules with a check mark (✅) are enabled by default while using the `plugin:cyp
| ✅ | [no-assigning-return-values](./docs/rules/no-assigning-return-values.md) | Prevent assigning return values of cy calls |
| ✅ | [no-unnecessary-waiting](./docs/rules/no-unnecessary-waiting.md) | Prevent waiting for arbitrary time periods |
| ✅ | [no-async-tests](./docs/rules/no-async-tests.md) | Prevent using async/await in Cypress test case |
| ✅ | [no-empty-tests](./docs/rules/no-empty-tests.md) | Prevent the execution of empty tests |
| | [no-force](./docs/rules/no-force.md) | Disallow using `force: true` with action commands |
| | [assertion-before-screenshot](./docs/rules/assertion-before-screenshot.md) | Ensure screenshots are preceded by an assertion |
| | [require-data-selectors](./docs/rules/require-data-selectors.md) | Only allow data-\* attribute selectors (require-data-selectors) |
Expand Down
38 changes: 38 additions & 0 deletions docs/rules/no-empty-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Checks if tests are empty (no-empty-tests)

Empty tests will always pass.
In large testsuites tests might have been initialized but not yet written.
Passing but nevertheless empty tests will suggest functionality has been tested, although it hasn't.
Having empty tests might seem convenient for reasons, it isn't on execution though.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Having empty tests might seem convenient for reasons, it isn't on execution though.

Empty tests should always be deleted or skipped.

## Rule Details

This rule aims to prevent the execution of empty tests.

Examples of **incorrect** code for this rule:

```js
it('an empty test', () => {} )
describe('nested', () => { it('empty test', () => { } ) })
context('nested', () => { it('empty test', () => { } ) })
test('an empty test', () => {} )
describe('nested', () => { test('empty test', () => { } ) })
context('nested', () => { test('empty test', () => { } ) })
```

Examples of **correct** code for this rule:

```js
foo.bar('random empty function', () => {})
it.skip('a skipped empty test', () => {} )
it('do something', () => { cy.dataCy('getter') } )
describe.skip('nested skip', () => { it('empty test', () => { } ) })
context.skip('nested skip', () => { it('empty test', () => { } ) })
describe.skip('nested skip', () => { context('nested', () => { it('empty test', () => { } ) }) })
test.skip('a skipped empty test', () => {} )
test('do something', () => { cy.dataCy('getter') } )
describe.skip('nested skip', () => { test('empty test', () => { } ) })
context.skip('nested skip', () => { test('empty test', () => { } ) })
describe.skip('nested skip', () => { context('nested', () => { test('empty test', () => { } ) }) })
```
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
'require-data-selectors': require('./lib/rules/require-data-selectors'),
'no-force': require('./lib/rules/no-force'),
'no-pause': require('./lib/rules/no-pause'),
'no-empty-tests': require('./lib/rules/no-empty-tests'),
},
configs: {
recommended: require('./lib/config/recommended'),
Expand Down
1 change: 1 addition & 0 deletions lib/config/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ module.exports = {
'cypress/no-assigning-return-values': 'error',
'cypress/no-unnecessary-waiting': 'error',
'cypress/no-async-tests': 'error',
'cypress/no-empty-tests': 'error',
},
}
92 changes: 92 additions & 0 deletions lib/rules/no-empty-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use strict'

module.exports = {
meta: {
docs: {
description: 'Checks for empty tests',
category: 'Possibly Errors',
recommended: 'error',
},
fixable: 'code',
hasSuggestions: true,
messages: {
unexpected: 'Do not keep empty tests',
skipTest: 'Skip the test',
removeTest: 'Remove the test',
},
},

create (context) {

function addSkip (node) {
return (fixer) => fixer.insertTextAfter(node.callee, '.skip')
}

function removeTest (node) {
return (fixer) => fixer.remove(node)
}

return {
CallExpression (node) {
if (isEmptyTest(node) && !areParentsSkipped(node)) {
return context.report({
node,
messageId: 'unexpected',
fix: addSkip(node),
suggest: [
{
messageId: 'skipTest',
fix: addSkip(node),
},
{
messageId: 'removeTest',
fix: removeTest(node),
},
],
})
}
},
}
},
}

const checkNode = (names) => {
return (node) => {
return node.type === 'CallExpression' && (
(node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
names.includes(node.callee.object.name)) ||
(node.callee.type === 'Identifier' && names.includes(node.callee.name)))
}
}

const isTest = checkNode(['test', 'it'])
const isGroup = checkNode(['describe', 'context'])

function isEmptyTest (node) {
return isTest(node) && !isSkipped(node) && isFunctionEmpty(node)
}

function isFunctionEmpty (node) {
return ['ArrowFunctionExpression', 'FunctionExpression', 'Identifier']
.includes(node.arguments[1].type) &&
node.arguments[1].body.body.length === 0
}

function isSkipped (node) {
return node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'skip'
}

function areParentsSkipped (node) {
while (node.parent) {
node = node.parent
if (isGroup(node) && isSkipped(node)) {
return true
}
}

return false
}
34 changes: 34 additions & 0 deletions tests/lib/rules/no-empty-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict'

const rule = require('../../../lib/rules/no-empty-tests')
const RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester()

const errors = [{ messageId: 'unexpected' }]
const parserOptions = { ecmaVersion: 6 }

ruleTester.run('no-empty-tests', rule, {
valid: [
{ code: 'foo.bar(\'random empty function\', () => {})', parserOptions },
{ code: 'it.skip(\'a skipped empty test\', () => {} )', parserOptions },
{ code: 'it(\'do something\', () => { cy.dataCy(\'getter\') } )', parserOptions },
{ code: 'describe.skip(\'nested skip\', () => { it(\'empty test\', () => { } ) })', parserOptions },
{ code: 'context.skip(\'nested skip\', () => { it(\'empty test\', () => { } ) })', parserOptions },
{ code: 'describe.skip(\'nested skip\', () => { context(\'nested\', () => { it(\'empty test\', () => { } ) }) })', parserOptions },
{ code: 'test.skip(\'a skipped empty test\', () => {} )', parserOptions },
{ code: 'test(\'do something\', () => { cy.dataCy(\'getter\') } )', parserOptions },
{ code: 'describe.skip(\'nested skip\', () => { test(\'empty test\', () => { } ) })', parserOptions },
{ code: 'context.skip(\'nested skip\', () => { test(\'empty test\', () => { } ) })', parserOptions },
{ code: 'describe.skip(\'nested skip\', () => { context(\'nested\', () => { test(\'empty test\', () => { } ) }) })', parserOptions },
],

invalid: [
{ code: 'it(\'an empty test\', () => {} )', parserOptions, errors },
{ code: 'describe(\'nested\', () => { it(\'empty test\', () => { } ) })', parserOptions, errors },
{ code: 'context(\'nested\', () => { it(\'empty test\', () => { } ) })', parserOptions, errors },
{ code: 'test(\'an empty test\', () => {} )', parserOptions, errors },
{ code: 'describe(\'nested\', () => { test(\'empty test\', () => { } ) })', parserOptions, errors },
{ code: 'context(\'nested\', () => { test(\'empty test\', () => { } ) })', parserOptions, errors },
],
})