Skip to content
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,15 @@ To enable this configuration use the `extends` property in your

## Supported Rules

| Rule | Description | Configurations | Fixable |
| -------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------- | ------------------ |
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [prefer-expect-query-by](docs/rules/prefer-expect-query-by.md) | Disallow the use of `expect(getBy*)` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| Rule | Description | Configurations | Fixable |
| -------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------- | ------------------ |
| [await-async-query](docs/rules/await-async-query.md) | Enforce async queries to have proper `await` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [await-fire-event](docs/rules/await-fire-event.md) | Enforce async fire event methods to be awaited | ![vue-badge][] | |
| [no-await-sync-query](docs/rules/no-await-sync-query.md) | Disallow unnecessary `await` for sync queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [no-get-by-assert](docs/rules/no-get-by-assert.md) | Disallow `getBy*` queries as assertion method itself | | |
| [prefer-expect-query-by](docs/rules/prefer-expect-query-by.md) | Disallow the use of `expect(getBy*)` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |

[build-badge]: https://img.shields.io/travis/Belco90/eslint-plugin-testing-library?style=flat-square
[build-url]: https://travis-ci.org/belco90/eslint-plugin-testing-library
Expand Down
51 changes: 51 additions & 0 deletions docs/rules/no-get-by-assert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Disallow `getBy*` queries as assertion method itself (no-get-by-assert)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The rule name sounds a bit strange to me. When it comes to preferences, ESLint rules are usually named prefer-..., I think it should be the case here.
no-get-by-assert implies here that doing assertions with getBy queries is wrong. I think something like prefer-query-by-assert is more clear to the user as discussed in the issue

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm happy to rename this to prefer-* rule but I would avoid using query-by for same reason I commented before, so I would go for something like prefer-explicit-assert. What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

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

prefer-explicit-assert sounds good to me!


Testing Library `getBy*` queries throw an error if the element is not
found. Some users like this behavior to use the query itself as an
assert for the element existence in their tests, but other users don't
and prefer to explicitly assert the element existence, so this rule is
for users from the latter.

## Rule Details

This rule aims to encourage users to explicitly assert existence of
elements in their tests rather than just use `getBy*` queries and expect
it doesn't throw an error so it's easier to understand what's the
expected behavior within the test.

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

```js
// just calling `getBy*` query expecting not to throw an error as an
// assert-like method, without actually using the returned element
getByText('foo');
```

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

```js
// wrapping the get query within a `expect` and use some matcher for
// making the assertion more explicit
expect(getByText('foo')).toBeDefined();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't it be expect(queryByText) instead of expect(getByText)? 🤔
Such an example would be conflicting with the prefer-expect-query-by rule.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's actually from a different rule so I would avoid having this one encouraging two things: asserting explicitly and asserting with queryBy*, so just leaving this rule for the former and prefer-expect-query-by for the latter.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, but as prefer-expect-query-by is in the recommended rules, I found it a little bit weird to say expect(getBy) is correct (although it is for this rule technically speaking) 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, in the example of this specific rule that's fine. It would be weird to see expect(queryByText('foo')).toBeDefined(); when the rule only talk about getBy*, so I wanted to clarify that specific use is valid for this rule.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sure! What about putting a side note stating that though expect(getBy) is valid for this rule, it is recommended to use queryBy for asserting elements' presence? Then, the user would know it's valid to use expect(getBy) while still being aware it's not recommended. What do you think? 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

That sounds absolutely reasonable. I'll address the feedback here later.

Copy link
Contributor

Choose a reason for hiding this comment

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

My 2 cents: I understand both points. However, since this is a documentation to something that is to be considered "valid", I would expect that we write things that are "valid", which is not really the case with expect(getBy), as we also have a recommended rule for it.

At least we should mention to wrap it into an expect and then to change it to queryBy, and link it to the other rule for more reference.


// even more explicit if you use `@testing-library/jest-dom` matcher
// for checking the element is present in the document
expect(getByText('foo')).toBeInTheDocument();

// Doing something with the element returned without asserting is absolutely fine
await waitForElement(() => getByText('foo'));
fireEvent.click(getByText('bar'));
const quxElement = getByText('qux');

// call directly something different than Testing Library query
getByNonTestingLibraryVariant('foo');
```

## When Not To Use It

If you prefer to use `getBy*` queries implicitly as an assert-like
method itself, then this rule is not recommended.

## Further Reading

- [getBy query](https://testing-library.com/docs/dom-testing-library/api-queries#getby)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const rules = {
'no-await-sync-query': require('./rules/no-await-sync-query'),
'no-debug': require('./rules/no-debug'),
'no-dom-import': require('./rules/no-dom-import'),
'no-get-by-assert': require('./rules/no-get-by-assert'),
'prefer-expect-query-by': require('./rules/prefer-expect-query-by'),
};

Expand Down
51 changes: 51 additions & 0 deletions lib/rules/no-get-by-assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';

const { getDocsUrl } = require('../utils');

const isDirectlyCalledByFunction = node =>
node.parent.parent.type === 'CallExpression';

const isReturnedByArrowFunctionExpression = node =>
node.parent.parent.type === 'ArrowFunctionExpression';

const isDeclared = node => node.parent.parent.type === 'VariableDeclarator';

const isReturnedByReturnStatement = node =>
node.parent.parent.type === 'ReturnStatement';

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallow `getBy*` queries as assertion method itself',
category: 'Best Practices',
recommended: false,
url: getDocsUrl('no-get-by-assert'),
},
messages: {
noGetByAssert: 'Disallowed use of `getBy*` query as implicit assert',
Copy link

Choose a reason for hiding this comment

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

Is this the message user will see on hover in VSCode for example? Should it include some information to use the expect(queryBy*)... pattern?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this is the message you'll get when linting the code (or your IDE pass the linter for you). To be honest I don't know what to put here, so I just put something simple mirroring other rules from ESLint. Suggestions are more than welcome.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would go for something like Use queryBy assertions instead of getBy. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

Same as previous mentions to queryBy: user can't be forced to do that, we can only mention the fact that the getBy must be transformed into explicit assert.

},
fixable: null,
schema: [],
},

create: function(context) {
return {
[`CallExpression > Identifier[name=${/^getBy(LabelText|PlaceholderText|Text|AltText|Title|DisplayValue|Role|TestId)$/}]`](
Copy link

Choose a reason for hiding this comment

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

Could this be extendable by the user if needed? For example we have some custom selectors e.g. getByIcon that I would like to fall under the same rule but should not be part of the default configuration.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I can make it extendable so users can add their own queries. I always have doubts about just checking getBy* default queries + custom ones or check everything starting by getBy, but I think I prefer the first one and make it extendable so you and other users can add custom queries and avoid false positives with other things.

node
) {
if (
!isDirectlyCalledByFunction(node) &&
!isReturnedByArrowFunctionExpression(node) &&
!isDeclared(node) &&
!isReturnedByReturnStatement(node)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I really like this approach. It makes the edge cases clear and readable 👌

Copy link
Member Author

Choose a reason for hiding this comment

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

I like it too, but I'm always concerned about if I left one edge case out 😨

) {
context.report({
node,
messageId: 'noGetByAssert',
});
}
},
};
},
};
73 changes: 73 additions & 0 deletions tests/lib/rules/no-get-by-assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';

const rule = require('../../../lib/rules/no-get-by-assert');
const { ALL_QUERIES_METHODS } = require('../../../lib/utils');
const RuleTester = require('eslint').RuleTester;

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } });
ruleTester.run('no-get-by-assert', rule, {
valid: [
{
code: `getByText`,
},
{
code: `expect(getByText('foo')).toBeDefined()`,
},
{
code: `expect(getByText('foo')).toBeInTheDocument();`,
},
{
code: `async () => { await waitForElement(() => getByText('foo')) }`,
},
{
code: `fireEvent.click(getByText('bar'));`,
},
{
code: `const quxElement = getByText('qux')`,
},
{
code: `() => { return getByText('foo') }`,
},
{
code: `function bar() { return getByText('foo') }`,
},
{
code: `getByNonTestingLibraryVariant('foo')`,
},
],

invalid: [
...ALL_QUERIES_METHODS.map(queryMethod => ({
code: `get${queryMethod}('foo')`,
errors: [
{
messageId: 'noGetByAssert',
},
],
})),
...ALL_QUERIES_METHODS.map(queryMethod => ({
code: `() => {
get${queryMethod}('foo')
doSomething()

get${queryMethod}('bar')
const quxElement = get${queryMethod}('qux')
}
`,
errors: [
{
messageId: 'noGetByAssert',
line: 2,
},
{
messageId: 'noGetByAssert',
line: 5,
},
],
})),
],
});