Should you render components / select elements in each `test()/it()` block or globally? - reactjs

In react-testing-library you have to render your react component before executing some tests on its elements.
For several tests on the same component, should you avoid
rendering the component multiple times? Or do you have to render it in each
test()/it() block?
Should you select elements of the component (e.g. button) in each test()/it() block, or should you lift the selection, and select only once?
Does it have any impact on the execution time of the tests?
Is one of the approaches a best practice/antipattern?
Why does the last example fail?
For the basic component I have the following testing approaches:
function MyComponent() {
return (
<>
<button disabled>test</button>
<button disabled>another button</button>
</>
);
}
e.g.
describe("MyComponent", () => {
it("renders", async () => {
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
expect(button).toBeInTheDocument();
});
it("is disabled", async () => {
// repetetive render and select, should be avoided or adopted?
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
expect(button).toBeDisabled();
});
});
vs.
describe("MyComponent", () => {
const { getByRole } = render(<MyComponent />);
const button = getByRole("button", { name: /test/i });
it("renders", async () => {
expect(button).toBeInTheDocument();
});
it("is disabled", async () => {
expect(button).toBeDisabled();
});
});
I would expect the second approach to have a faster execution time since the component has to be rendered only once, but I don't know how to measure it and if it is an anti-pattern?
While it seems to be more DRY, if I add another toBeInTheDocument check, it fails.
Why is this the case?
describe("MyComponent", () => {
const { getByRole } = render(<MyComponent />);
const button = screen.getByRole("button", { name: /test/i });
const button2 = screen.getByRole("button", { name: /another button/i });
it("renders", async () => {
expect(button).toBeInTheDocument(); //ok
});
it("is disabled", async () => {
expect(button).toBeDisabled(); // ok
});
it("renders second button", async () => {
expect(button2).toBeInTheDocument(); // fails: element could not be found in the document
});
});
So this approach seems to be more error-prone!?

Each test should be as atomic as possible, meaning that it should not be using anything that other tests are also using and should run with a fresh state. So relating that to your examples, the first one would be the correct pattern.
When you have a test suite that contains sharable state between unit tests e.g. objects or environment variables, the test suite is very prone to errors. The reason for that is; if one of the unit tests happens to mutate one of the shared objects; all of the other unit tests will also be affected by this, causing them to exhibit unwanted behaviour. This can result in test failures where the code is technically correct or even set up landmines for future developers where the addition of new tests which are correct would still result in failures, hence causing major headaches in figuring out why this is happening.
The only exception to this rule would be immutable primitive variables (e.g. string, number, boolean with the use of const keyword) as tests will not be able to mutate them and they are useful for storing reusable ids, text etc.
Ofcourse, repeating the setup of each unit test can make them really clunky, that's why jest offers the beforeEach, beforeAll, afterEach and afterAll functions to extract the repeating logic. However, this opens up the vulnerability of shared state, so do be careful and make sure that all state is refreshed before any tests are kicked off. Ref.
For the last question as to why your last unit test in the last example is failing - it appears that you are using getByRole to look for button text. You should be using getByText instead. getByRole is used with role attributes (e.g. <button role="test">test</button>) which you don't seem to be using.

Related

Is it necessary to call unmount after each test cases in react testing libarary?

describe('<CustomTooltip />', () => {
it('should show tooltip text', async () => {
const { container, unmount } = render(<CustomTooltip text='Tooltip Text' />)
userEvent.hover(container.querySelector('svg'))
await screen.findByText('Tooltip Text')
screen.debug()
unmount() // ?? is it necessary to call unmount after each test cases?
})
it('check if there is an mounted component', () => {
screen.debug()
})
})
Is it necessary to call unmount after each test cases? Because I've added useEffect in the CustomTooltip component, and Unmounted is logged before the second test case. And even the second test case screen.debug output is <body />.
useEffect(() => {
console.log('Mounted')
return () => console.log('Unmounted')
}, [])
I asked this because i saw a custom implementation for render in test utils to unmount component after each test cases, and I'm curious to know if this is really important.
let lastMount = null;
const _render = (...args) => {
lastMount = render(...args);
return lastMount;
};
afterEach(() => {
if (lastMount)
lastMount.unmount();
lastMount = null;
});
It depends on the testing framework that you are using. If you are using jest, for instance, it's not needed.
Here is a reference to this suggestion https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#using-cleanup
For a long time now cleanup happens automatically (supported for most major testing frameworks) and you no longer need to worry about it

Test functionality with timeouts in react-testing-library and jest

I'm trying to follow TDD, and I have a span that should appear on screen after 5 seconds. I haven't implemented the span at all, so the test should fail, but currently it passes the test expect(messageSpan).toBeInTheDocument.
Here are my two tests:
it("doesn't show cta message at first", () => {
render(<FAB />);
const messageSpan = screen.queryByText(
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeNull(); // passes, as it should
});
it("should show the cta message after 5 secs", () => {
render(<FAB />);
setTimeout(() => {
const messageSpan = screen.getByText( // also tried queryByText instead of get
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeInTheDocument(); // also passes, even though messageSpan should throw an error.
}, 5000);
});
Here's my FAB component, where you can see there's no message at all:
export default function FAB() {
return (
// using styled-components; there's no content in any of these.
<StyledFABContainer>
<StyledFABButton>
<BriefcaseIcon />
</StyledFABButton>
</StyledFABContainer>
);
}
To complicate things, I don't plan to have a set function I call for the setTimeout. I will simply set state after a set time of 5 secs. So don't think I can use the suggestions in the timer mocks section of jest docs: https://jestjs.io/docs/timer-mocks
My two questions are:
a) Why would this pass and not throw an error/null?
b) How can I properly test setTimeout functionality in RTL?
UPDATE: Have tried using various combinations of useFakeTimers, act, waitFor etc., but no luck. Here's my current test as it's written out, and throwing two errors - one saying I need to use act when changing state (which I am, but still) and one saying my messageSpan is null:
it("cta message to display after 5 secs", async () => {
jest.useFakeTimers();
const el = document.createElement("div");
act(() => {
ReactDOM.render(<FAB />, el);
});
jest.advanceTimersByTime(5000);
const messageSpan = screen.queryByText(
"Considering a career in nursing? Join our team!"
);
expect(messageSpan).toBeInTheDocument();
});
You can not use setTimeout like this in your tests. An obvious reason would be that you do not want to wait 5 seconds in your test to then continue. Imagine a component that would change after 10min. You cant wait that long but should use jests mocked timers API instead.
You can progress the nodejs timer so your component changes, then immediately make your assertion.
Other remarks about the tests you wrote:
If you are awaiting changes you should use await waitFor(() => ...) from testing library. It checks the expectations to pass every 50ms by default for a total of 5sec. If all expectations pass it continues. You should make asynchronous assertions there.
"expect(messageSpan).toBeNull()" should be "expect(messageSpan).not.toBeInTheDocument()". Its good to go with the accessibility way testing library provides.
If working with an asynchronous test I found a solution on this blog: https://onestepcode.com/testing-library-user-event-with-fake-timers/
The trick is to set the delay option on the userEvent to null.
const user = userEvent.setup({ delay: null });
Here is a full test case
test("Pressing the button hides the text (fake timers)", async () => {
const user = userEvent.setup({ delay: null });
jest.useFakeTimers();
render(<Demo />);
const button = screen.getByRole("button");
await user.click(button);
act(() => {
jest.runAllTimers();
});
const text = screen.queryByText("Hello World!");
expect(text).not.toBeInTheDocument();
jest.useRealTimers();
});

React & Enzyme/Jest: beforeEach() to group variables & use before in each unit test best practice?

I am working on my first react unit test and would like to know if there is a best practice for grouping variables used in every unit test? I have a group of unit tests for a form that uses the same variables. I grouped them in an 'describe(){}' and would like to have the variables at the beginning of each test. My approach is below, but I am receiving an error that says 'ReferenceError: input is not defined'.
If I do not wrap them in a beforeEach(), then I receive errors for the 'screen.getByText' lines that it was 'Unable to find an element with the text' even though it's wrapped in an await.
The tests run fine if I have the variables repeated in each test, but that would be a lot of duplicated code as I have 6 tests within the describe().
import { render, fireEvent, wait, cleanup, screen } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
describe('testing subscription form validation', () => {
beforeEach(() => {
let handleChange = jest.fn();
let {getByTestId} = render(<Form handleChange={handleChange}/>);
let input = getByTestId('test-input');
let button = screen.getByTestId('test-button');
});
it('[some test description]', async () => {
fireEvent.change(input, { target: { value: '[test value]' } });
fireEvent.click(button);
expect(screen.getByText('[button text]')).toBeInTheDocument();
await wait(() => expect(input).toHaveAttribute('value', 'test value'));
});
it('[another test]', async () => {
fireEvent.change(input, { target: { value: '' } });
fireEvent.click(button);
await wait(() => {
const requiredText = screen.getByText('This field is required');
expect(requiredText).toBeInTheDocument();
});
});
});
Thank you for taking the time to help! Any guidance would be great.
You should dclare the variables outside the function and assign to them inside your beforeEach(), like so:
describe('hdgsbdicg', () => {
let input;
bedoreEach(() => {
input=jest.fn(); //or whatever
}
});

How do i use a single render across multiple tests in React Testing Library.?

Is it possible to maintain the same render() across multiple tests in using Jest and React Testing Library? I am building a Trivia App and have a single component that displays different questions as the user progresses through the quiz. In order to test the functionality of the choice buttons, the submit button, and checking that the right screens are displayed at the right time, I need to perform tests on the same rendered component at different stages of the quiz. For instance:
describe("Question Screen", () => {
it("should render the first question when difficulty button is clicked", async () => {
render(<TriviaBox/>);
const btn = screen.getByRole("button", {name: /easy/i});
fireEvent.click(btn);
const heading = await screen.findByText("Question 1");
expect(heading).toBeInTheDocument();
});
it("should display the next question when the current question is answered", async () => {
render(<TriviaBox/>);
const btn = screen.getByRole("button", {name: /easy/i});
fireEvent.click(btn);
const correctAnswer = await screen.findByRole("button", {name: /Nevada/i});
const submit = await screen.findByRole("button", {name: /Submit/i});
fireEvent.click(correctAnswer);
fireEvent.click(submit);
expect(wait screen.findByText("Question 2")).toBeInTheDocument();
expect(wait screen.findByText("Which is the largest state?")).toBeInTheDocument();
expect(wait screen.findAllByRole("radio")).toHaveLength(4);
...
});
});
Is there a way to preserve the same render from the first test for use in the second test, rather than having to re-render the same component and step through the first question again in order to test the second question?
Basically what you need is to disable auto cleanup because it unmounts React trees after each test.
See docs: https://testing-library.com/docs/react-testing-library/setup/#skipping-auto-cleanup.
But in this case you should care about calling cleanup manually to not compromise next tests.
Here is a small working example of how to do this with importing "#testing-library/react/dont-cleanup-after-each":
import "#testing-library/react/dont-cleanup-after-each";
import { render, screen, cleanup } from "#testing-library/react";
function TestComponent() {
return (
<div>
<p>First element</p>
<p>Second element</p>
</div>
);
}
describe("TestComponent", () => {
afterAll(() => {
cleanup();
});
it("should contain `First element` text", () => {
render(<TestComponent />);
screen.getByText("First element");
});
it("should contain `Second element` text", () => {
screen.getByText("Second element");
});
});
One method to do this is to write a beforeAll function to initialise the render. That will initialise only once for all child tests.
describe("Question Screen", () => {
beforeAll(() => {
render(<TriviaBox/>);
})
it("should render the first question when difficulty button is clicked", async () => {
const btn = screen.getByRole("button", {name: /easy/i});
fireEvent.click(btn);
const heading = await screen.findByText("Question 1");
expect(heading).toBeInTheDocument();
});
...
});
See the JEST docs https://jestjs.io/docs/en/setup-teardown#one-time-setup

React Testing Library - testing in isolation and userEvent error

I'm writing tests using Jest and React Testing Library. I had failing tests and I realized that if I change the order of the tests, it will work. I'm guessing this is because the tests aren't properly isolated and one test might affect the other.
I am calling:
afterEach(() => {
cleanup()
jest.resetAllMocks()
})
I have a test that looks like this:
it('calls API when submitted', async () => {
render(<SignUp />)
fillAllVerificationFieldsWithTestData()
validateUser.mockResolvedValueOnce({ id: 123 })
const signupButton = screen.getByTestId(
'sign-up-verification-button',
)
userEvent.click(signupButton)
await waitFor(() => expect(validateUser).toHaveBeenCalledTimes(1))
})
If I create the exact same test or run a similar test after this one with a userEvent.click, I get an error:
Unable to fire a "mouseMove" event - please provide a DOM element.
I looked in the #testing-library/user-event library and I see this code:
const userEvent = {
click(element) {
const focusedElement = element.ownerDocument.activeElement;
const wasAnotherElementFocused =
focusedElement !== element.ownerDocument.body &&
focusedElement !== element;
if (wasAnotherElementFocused) {
fireEvent.mouseMove(focusedElement);
fireEvent.mouseLeave(focusedElement);
}
I noticed that element.ownerDocument.activeElement is null wasAnotherElementFocused is true and so it throws the error.
The first time I run the test it isn't null so it works.
Do I need some extra clean up between tests? If I use fireEvent:
fireEvent(signupButton,
new MouseEvent('click', {
bubbles: true,
}),
)
It works but I'm afraid I'm doing something wrong and not isolating my tests correctly.
EDIT:
Here is the code for the fillAllVerificationFieldsWithTestData:
export const fillAllVerificationFieldsWithTestData = () => {
const { given_name, family_name, zip, social, loanNumber } = {
given_name: screen.getByTestId('given_name'),
family_name: screen.getByTestId('family_name'),
zip: screen.getByTestId('zip'),
social: screen.getByTestId('last4ssn'),
loanNumber: screen.getByTestId('loan_number'),
}
userEvent.type(given_name, 'FirstName')
userEvent.type(family_name, 'LastName')
userEvent.type(zip, '77025')
userEvent.type(social, '1234')
userEvent.type(loanNumber, '1112223333')
}
and screen is imported from #testing-library/react and I import validate user like this:
import { validateUser } from '../../../services/auth'
jest.mock('../../../services/auth')
So, I just faced this problem, and what fixed it for me was wrapping any code that causes a React state change (which your userEvent code presumably does) in act. So, your initial test would look like this:
it('calls API when submitted', async () => {
render(<SignUp />)
fillAllVerificationFieldsWithTestData()
validateUser.mockResolvedValueOnce({ id: 123 })
const signupButton = screen.getByTestId(
'sign-up-verification-button',
)
await act(() => userEvent.click(signupButton))
await waitFor(() => expect(validateUser).toHaveBeenCalledTimes(1))
})
Can you give this a try, applying similar changes to your fillAllVerificationFieldsWithTestData function, and let us know how it turned out?
This is a bug that was reported in the testing-library/user-event GitHub and it should be fixed as of v11.0.1. It's specific to userEvent, which is why fireEvent works.
Note that you shouldn't need to call cleanup if you're using Jest because it will get called for you.

Resources