React Testing Library - Avoid getBy?
Asked Answered
N

2

22

When testing components with React Testing Library, I find myself starting with getBy*, and occasionally needing to replace it with queryBy* (for example if I need to check for the non-existence of an element). My tests end up with a mix of getBy and queryBy, and I've recently just been using queryBy for everything.

It got me thinking... is there ever a reason to use getBy?

Assertions like this fail as expected, without the need to throw an error:

expect(queryByText('Click me')).toBeInTheDocument();
expect(queryByLabel('Name').value).toBe('George')

What's the advantage of throwing an error if an element isn't found, and is there a reason not to use queryBy for all (synchronous) queries?

EDIT: It looks like queryBy is now only recommended for asserting that something is not in the document:

https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#using-query-variants-for-anything-except-checking-for-non-existence

The article also recommends using screen.queryBy/screen.getBy rather than destructuring from render, which simplifies changing from one to another, since you no longer have to update the destructured function.

Necrophilia answered 14/11, 2019 at 12:39 Comment(0)
C
33

As you've stated, the difference between getBy* and queryBy* is that getBy* throws an error if the element is not found and queryBy* does not. For me, if I'm expecting something to be there, I always use getBy* and only use queryBy* in scenarios where I'm asserting that something isn't there. If an element isn't present that I'm expecting to be, I want to know about it as early as possible in the test, which is wherever the getBy* call is made.

So I would say the advantage of throwing the error is that you always ensure your test failure will point to the root problem (not being able to find an element you expect to be there) as opposed to a side effect of that root problem (trying to use that element for something later in the test).

Example Test:

    const { getByTestId, queryByTestId } = render(getComponent());

    const textInput = queryByTestId("textInput");

    fireEvent.change(textInput, { target: { value: "hello" } });
    fireEvent.change(textInput, { target: { value: "hi" } });

Using queryByTestId, the test output is:

Unable to fire a "change" event - please provide a DOM element.

      23 |     const textInput = queryByTestId("textInput") as any;
      24 |
    > 25 |     fireEvent.change(textInput, { target: { value: "hello" } });
         |               ^
      26 |     fireEvent.change(textInput, { target: { value: "hi" } });
      27 |

So it does indicate that textInput wasn't found. If I change it to getByTestId, the output is

Unable to find an element by: [data-testid="textInput"]

    <body>
      <div>
        <div>
          <button
            type="button"
          >
            Show the Text Input!
          </button>
        </div>
      </div>
    </body>

      21 |     const { getByTestId, queryByTestId, rerender } = render(getComponent());
      22 |
    > 23 |     const textInput = getByTestId("textInput") as any;
         |                       ^
      24 |
      25 |     fireEvent.change(textInput, { target: { value: "hello" } });
      26 |     fireEvent.change(textInput, { target: { value: "hi" } });

So the getBy* error output has two advantages in my mind:

  1. It points directly to the line that is the problem. It's not too hard to figure out that querying for the "textInput" is the problem in the first case. But it is a bit less direct.
  2. It automatically prints what the DOM looks like when I use getBy*. This can be helpful when determining why something I'm looking for isn't present. Once the queryBy* test fails, that's likely one of the first steps I'm going to take anyways, so it's nice that it's just there automatically.

These marginal dev experience improvements are worth using the getBy* varieties as the default for me. Typically I'm only using getBy* in my tests and only using queryBy* when it's important to assert that something isn't present. It is certainly possible to just use queryBy* though and you are free to do so if you find the cost of using both outweighs the benefits.

Cist answered 14/11, 2019 at 13:4 Comment(3)
But queryBy* works great when you're expecting something to be there! If it doesn't find anything, it will fail as soon as you try to assert on it or access a property, with an error message pointing to the root problem. Then you don't have to destructure both getBy and queryBy, and don't have to keep 2 slightly different APIs straight. I just haven't yet run into a case that getBy is a better tool than queryBy.Necrophilia
Added an example to try and make the advantages I see with getBy* a bit more concrete. For me, these marginal benefits outweigh the marginal costs you mention of keeping the 2 different APIs straight, but they may not for you.Cist
That's helpful! In general, the message Unable to fire a "change" event - please provide a DOM element. is enough info for me, but having the DOM printed without debug() when using getBy* is a benefit I hadn't thought of. Thanks for your thoughtful answer!Necrophilia
G
5

According to React Testing Library's creator Kent C. Dodds. You shouldn't be using queryBy* except to test for non existence.

The only reason the query* variant of the queries is exposed is for you to have a function you can call which does not throw an error if no element is found to match the query (it returns null if no element is found). The only reason this is useful is to verify that an element is not rendered to the page. The reason this is so important is because the get* and find* variants will throw an extremely helpful error if no element is found–it prints out the whole document so you can see what's rendered and maybe why your query failed to find what you were looking for. Whereas query* will only return null and the best toBeInTheDocument can do is say: "null isn't in the document" which is not very helpful.

source

Gareri answered 31/1, 2023 at 5:35 Comment(2)
Thanks! I edited my question a while back with the same link :)Necrophilia
It's still weird how you do getBy* and then toBeVisible (or toBeInTheDocument) if getBy itself is enough to fail. Or are those toBe* calls not recommended?Sostenuto

© 2022 - 2024 — McMap. All rights reserved.