3
\$\begingroup\$

Take this basic component using state:

export const InputChild = () => {
  const [count, setCount] = useState(0);
  const updateCount = () => setCount((prev) => prev + 1);

  return (
    <>
      <h1>Count: </h1>
      <h2>{count}</h2>
      <button onClick={updateCount}>Click</button>
    </>
  );
};

And this test that just ensures the count increases after clicking the button:

  it('should increment', () => {
    act(() => {
      render(<InputChild />);
    });

    let h2 = screen.getByRole('heading', { level: 2 });
    let count = Number(h2.innerHTML);
    const button = screen.getByText('Click');

    expect(count).toBe(0);
    expect(button).toBeInTheDocument();

    // fireEvent.click(button); // also works
    act(() => {
      button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
    });

    // count = Number(h2.innerHTML); // then use count below
    expect(Number(h2.innerHTML)).toBe(1);
    fireEvent.click(button);
    expect(Number(h2.innerHTML)).toBe(2);
    fireEvent.click(button);
    expect(Number(h2.innerHTML)).toBe(3);
  });

It’s annoying that I have to use Number(h2.innerHTML) in here in order to get the latest count value. Alternatively I could redefine count whenever clicking the button to get the latest innerHTML value (as the state changes):

count = Number(h2.innerHTML); 
expect(count).toBe(1);
fireEvent.click(button);
count = Number(h2.innerHTML);
expect(count).toBe(2);
fireEvent.click(button);
count = Number(h2.innerHTML);
expect(count).toBe(3);

But this seems too verbose.

Is there not a better way of doing this other than something like Number(h2.innerHTML)? I am new to unit testing and am trying to not write bad code, and the above just doesn’t feel right to me and I assume someone will have a better way of doing it.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Use const instead of let

let h2 = screen.getByRole('heading', { level: 2 });

Good that you're using screen and ByRole, but use const unless you plan to reassign the variable (and don't plan to reassign variables if you can possibly help it).

You could try getByRole for const button = screen.getByText('Click'); too. If you change the text here or introduce another element that has the text Click, that can cause the test to fail when it shouldn't.


Use fireEvent rather than native DOM events

// fireEvent.click(button); // also works
act(() => {
  button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});

I'd prefer using fireEvent unless you have a specific reason not to. It's already wrapped in act so you don't need to worry about that. Same goes for render, which should probably be broken out into a beforeEach block.


Use .toHaveTextContent() rather than .innerHTML

Instead of h2.innerHTML, you can use expect(h2).toHaveTextContent() brought to you by jest-dom. See Common mistakes with React Testing Library: Using the wrong assertion.

In general, the philosophy of RTL is not to be dipping into native DOM elements unless you have to.

I'd write expect(Number(h2.innerHTML)).toBe(1) as expect(h2.innerHTML).toBe("1"). If the assertion fails, you'll get a much clearer error that compares .innerHTML against the string, rather than whatever weird value Number() might have returned, like NaN. (but using toHaveTextContent() as described above avoids the problem altogether)


Minor points

expect(button).toBeInTheDocument(); is OK, but keep in mind that getBy will throw if it fails, so it's basically an always-pass. Kent recommends keeping the explicit assertion anyway but I figured I'd point it out.

I'd probably keep the assertion right after the variable assignment rather than interleaving them. For example,

let count = Number(h2.innerHTML);
const button = screen.getByText('Click');

expect(count).toBe(0);
expect(button).toBeInTheDocument();

would be:

const count = Number(h2.innerHTML);
expect(count).toBe(0);

const button = screen.getByText('Click');
expect(button).toBeInTheDocument();

or with jest-dom and other suggestions:

expect(h2).toHaveTextContent("0");

const button = screen.getByRole('Button');
expect(button).toBeInTheDocument();
\$\endgroup\$
3
  • \$\begingroup\$ great advice, thank you very much for your help! quick question: do you know what is best to use for button clicks? e.g. dispatchEvent vs fireEvent (as you pointed out) vs userEvent? It seems like there are many ways to skin a cat but the best way is unclear \$\endgroup\$ Commented Jul 6, 2022 at 21:37
  • 1
    \$\begingroup\$ Good question. Going back to Kent's article I linked throughout, there's a section: Not using @testing-library/user-event which mimics the user's actions better. TBH, I've just used fireEvent which has been sufficient for my unit testing needs. I've relied on E2E tests with Puppeteer/Cypress to provide that "real user" simulation with trusted events, but Kent's the expert, and it seems good to apply that philosophy to unit tests too. \$\endgroup\$
    – ggorlen
    Commented Jul 6, 2022 at 21:41
  • \$\begingroup\$ that's interesting to know. A course I also undertook also suggested just using userEvent over fireEvent, although it seems that they're both similar and that both would probably work. You can only get so far with unit tests and I am yet to explore e2e tests, and as you say, they will probably provide a better simulation for those cases \$\endgroup\$ Commented Jul 6, 2022 at 22:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.