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.
1 Answer 1
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();
-
\$\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\$user8758206– user87582062022年07月06日 21:37:37 +00:00Commented 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 usedfireEvent
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– ggorlen2022年07月06日 21:41:31 +00:00Commented 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\$user8758206– user87582062022年07月06日 22:03:15 +00:00Commented Jul 6, 2022 at 22:03
Explore related questions
See similar questions with these tags.