henk
henk

Reputation: 2838

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

In react-testing-library you have to render your react component before executing some tests on its elements.

  1. 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?

  2. 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?

  3. Does it have any impact on the execution time of the tests?

  4. Is one of the approaches a best practice/antipattern?

  5. 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!?

Upvotes: 3

Views: 2335

Answers (1)

Ovidijus Parsiunas
Ovidijus Parsiunas

Reputation: 2732

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.

Upvotes: 6

Related Questions