You are viewing documentation for v14 alpha. This version is under active development and APIs may change.

Common Mistakes with React Native Testing Library

Note: This guide is adapted from Kent C. Dodds' article "Common mistakes with React Testing Library" for React Native Testing Library v14. The original article focuses on web React, but the principles apply to React Native as well. This adaptation includes React Native-specific examples, async API usage (v14), and ARIA-compatible accessibility attributes.

React Native Testing Library guiding principle is:

"The more your tests resemble the way your software is used, the more confidence they can give you."

This guide outlines some common mistakes people make when using React Native Testing Library and how to avoid them.

Using the wrong query

Importance: high

React Native Testing Library provides several query types. Here's the priority order:

  1. Queries that reflect user experience:

    • getByRole - most accessible
    • getByLabelText - accessible label
    • getByPlaceholderText - TextInput placeholder text
    • getByText - text content
    • getByDisplayValue - TextInput input value
  2. Semantic queries:

    • getByTestId - only if nothing else works

Here's an example of using the right query:

import { TextInput, View } from 'react-native';
import { render, screen } from '@testing-library/react-native';

test('finds input by label', async () => {
  await render(
    <View>
      <TextInput aria-label="Username" placeholder="Enter username" value="" />
    </View>
  );

  // ✅ Good - uses accessible label
  const input = screen.getByLabelText('Username');

  // ✅ Also good - uses placeholder
  const inputByPlaceholder = screen.getByPlaceholderText('Enter username');

  // ❌ Bad - uses testID when accessible queries work
  // const input = screen.getByTestId('username-input');
});

Not using *ByRole query most of the time

Importance: high

getByRole is the most accessible query and should be your first choice. It queries elements by their semantic role:

import { Pressable, Text, TextInput, View } from 'react-native';
import { render, screen } from '@testing-library/react-native';

test('uses role queries', async () => {
  await render(
    <View>
      <Pressable role="button">
        <Text>Submit</Text>
      </Pressable>
      <TextInput role="searchbox" aria-label="Search" placeholder="Search..." />
    </View>
  );

  // ✅ Good - uses role query
  const button = screen.getByRole('button', { name: 'Submit' });
  const searchbox = screen.getByRole('searchbox', { name: 'Search' });

  expect(button).toBeOnTheScreen();
  expect(searchbox).toBeOnTheScreen();
});

Common roles in React Native include:

  • button - pressable elements
  • text - static text
  • header / heading - headers
  • searchbox - search inputs
  • switch - toggle switches
  • checkbox - checkboxes
  • radio - radio buttons
  • And more...

Note: React Native supports both ARIA-compatible (role) and legacy (accessibilityRole) props. Prefer role for consistency with web standards.

Using the wrong assertion

Importance: high

React Native Testing Library provides built-in Jest matchers. Make sure you're using the right ones:

import { Pressable, Text } from 'react-native';
import { render, screen } from '@testing-library/react-native';

test('button is disabled', async () => {
  await render(
    <Pressable role="button" aria-disabled>
      <Text>Submit</Text>
    </Pressable>
  );

  const button = screen.getByRole('button', { name: 'Submit' });

  // ✅ Good - uses RNTL matcher
  expect(button).toBeDisabled();

  // ❌ Bad - doesn't use RNTL matcher
  expect(button.props['aria-disabled']).toBe(true);
});

Common matchers include:

  • toBeOnTheScreen() - checks if element is rendered (replaces toBeInTheDocument())
  • toBeDisabled() - checks if element is disabled
  • toHaveTextContent() - checks text content
  • toHaveAccessibleName() - checks accessible name
  • And more...

Using query* variants for anything except checking for non-existence

Importance: high

Use queryBy* only when checking that an element doesn't exist:

import { View, Text } from 'react-native';
import { render, screen } from '@testing-library/react-native';

test('checks non-existence', async () => {
  await render(
    <View>
      <Text>Hello</Text>
    </View>
  );

  // ✅ Good - uses queryBy for non-existence check
  expect(screen.queryByText('Goodbye')).not.toBeOnTheScreen();

  // ❌ Bad - uses queryBy when element should exist
  // const element = screen.queryByText('Hello');
  // expect(element).toBeOnTheScreen();

  // ✅ Good - uses getBy when element should exist
  expect(screen.getByText('Hello')).toBeOnTheScreen();
});

Using waitFor to wait for elements that can be queried with find*

Importance: high

Use findBy* queries instead of waitFor + getBy*:

import { View, Text } from 'react-native';
import { render, screen, waitFor } from '@testing-library/react-native';

test('waits for element', async () => {
  const Component = () => {
    const [show, setShow] = React.useState(false);

    React.useEffect(() => {
      setTimeout(() => setShow(true), 100);
    }, []);

    return <View>{show && <Text>Loaded</Text>}</View>;
  };

  await render(<Component />);

  // ✅ Good - uses findBy query
  const element = await screen.findByText('Loaded');
  expect(element).toBeOnTheScreen();

  // ❌ Bad - uses waitFor + getBy
  // await waitFor(() => {
  //   expect(screen.getByText('Loaded')).toBeOnTheScreen();
  // });
});

Performing side-effects in waitFor

Importance: high

Don't perform side-effects in waitFor callbacks:

import { Pressable, Text, View } from 'react-native';
import { render, screen, waitFor, fireEvent } from '@testing-library/react-native';

test('avoids side effects in waitFor', async () => {
  const Component = () => {
    const [count, setCount] = React.useState(0);
    return (
      <View>
        <Pressable role="button" onPress={() => setCount(count + 1)}>
          <Text>Increment</Text>
        </Pressable>
        <Text>Count: {count}</Text>
      </View>
    );
  };

  await render(<Component />);

  const button = screen.getByRole('button');

  // ❌ Bad - side effect in waitFor
  // await waitFor(async () => {
  //   await fireEvent.press(button);
  //   expect(screen.getByText('Count: 1')).toBeOnTheScreen();
  // });

  // ✅ Good - side effect outside waitFor
  await fireEvent.press(button);
  await waitFor(() => {
    expect(screen.getByText('Count: 1')).toBeOnTheScreen();
  });
});

Using container to query for elements

Importance: high

React Native Testing Library provides a container object that has a queryAll method, but you should avoid using it directly:

import { View, Text } from 'react-native';
import { render } from '@testing-library/react-native';

test('finds element incorrectly', async () => {
  const { container } = await render(
    <View>
      <Text testID="message">Hello</Text>
    </View>
  );

  // ❌ Bad - using container.queryAll directly
  const element = container.queryAll((node) => node.props.testID === 'message')[0];

  // ✅ Good - use proper queries
  // const element = screen.getByTestId('message');
});

Instead, use the proper query methods from screen or the render result. The container is a low-level API that you rarely need.

Passing an empty callback to waitFor

Importance: high

Don't pass an empty callback to waitFor:

import { View } from 'react-native';
import { render, waitFor } from '@testing-library/react-native';

test('waits correctly', async () => {
  await render(<View testID="test" />);

  // ❌ Bad - empty callback
  // await waitFor(() => {});

  // ✅ Good - meaningful assertion
  await waitFor(() => {
    expect(screen.getByTestId('test')).toBeOnTheScreen();
  });
});

Not using screen

Importance: medium

You can get all the queries from the render result:

import { View, Text } from 'react-native';
import { render } from '@testing-library/react-native';

test('renders component', async () => {
  const { getByText } = await render(
    <View>
      <Text>Hello</Text>
    </View>
  );

  expect(getByText('Hello')).toBeOnTheScreen();
});

But you can also get them from the screen object:

import { View, Text } from 'react-native';
import { render, screen } from '@testing-library/react-native';

test('renders component', async () => {
  await render(
    <View>
      <Text>Hello</Text>
    </View>
  );

  expect(screen.getByText('Hello')).toBeOnTheScreen();
});

Using screen has several benefits:

  1. You don't need to destructure getByText from render
  2. It's more consistent with the Testing Library ecosystem

Wrapping things in act unnecessarily

Importance: medium

React Native Testing Library's render, renderHook, userEvent, and fireEvent are already wrapped in act, so you don't need to wrap them yourself:

import { Pressable, Text, View } from 'react-native';
import { render, fireEvent, screen } from '@testing-library/react-native';

test('updates on press', async () => {
  const Component = () => {
    const [count, setCount] = React.useState(0);
    return (
      <View>
        <Pressable role="button" onPress={() => setCount(count + 1)}>
          <Text>Count: {count}</Text>
        </Pressable>
      </View>
    );
  };

  await render(<Component />);

  const button = screen.getByRole('button');

  // ✅ Good - fireEvent is already wrapped in act
  await fireEvent.press(button);

  expect(screen.getByText('Count: 1')).toBeOnTheScreen();

  // ❌ Bad - unnecessary act wrapper
  // await act(async () => {
  //   await fireEvent.press(button);
  // });
});

Not using User Event API

Importance: medium

userEvent provides a more realistic way to simulate user interactions:

import { Pressable, Text, TextInput, View } from 'react-native';
import { render, screen, userEvent } from '@testing-library/react-native';

test('uses userEvent', async () => {
  const user = userEvent.setup();

  const Component = () => {
    const [value, setValue] = React.useState('');
    return (
      <View>
        <TextInput aria-label="Name" value={value} onChangeText={setValue} />
        <Pressable role="button" onPress={() => setValue('')}>
          <Text>Clear</Text>
        </Pressable>
      </View>
    );
  };

  await render(<Component />);

  const input = screen.getByLabelText('Name');
  const button = screen.getByRole('button', { name: 'Clear' });

  // ✅ Good - uses userEvent for realistic interactions
  await user.type(input, 'John');
  expect(input).toHaveValue('John');

  await user.press(button);
  expect(input).toHaveValue('');
});

userEvent methods are async and must be awaited. Available methods include:

  • press() - simulates a press
  • longPress() - simulates long press
  • type() - simulates typing
  • clear() - clears text input
  • paste() - simulates pasting
  • scrollTo() - simulates scrolling

Not querying by text

Importance: medium

In React Native, text is rendered in <Text> components. You should query by the text content that users see:

import { Text, View } from 'react-native';
import { render, screen } from '@testing-library/react-native';

test('finds text correctly', async () => {
  await render(
    <View>
      <Text>Hello World</Text>
    </View>
  );

  // ✅ Good - queries by visible text
  expect(screen.getByText('Hello World')).toBeOnTheScreen();

  // ❌ Bad - queries by testID when text is available
  // expect(screen.getByTestId('greeting')).toBeOnTheScreen();
});

Not using Testing Library ESLint plugins

Importance: medium

There's an ESLint plugin for Testing Library: eslint-plugin-testing-library. This plugin can help you avoid common mistakes and will automatically fix your code in many cases.

You can install it with:

yarn add --dev eslint-plugin-testing-library

And configure it in your eslint.config.js (flat config):

import testingLibrary from 'eslint-plugin-testing-library';

export default [testingLibrary.configs['flat/react']];

Note: Unlike React Testing Library, React Native Testing Library has built-in Jest matchers, so you don't need eslint-plugin-jest-dom.

Using cleanup

Importance: medium

React Native Testing Library automatically cleans up after each test. You don't need to call cleanup() manually unless you're using the pure export (which doesn't include automatic cleanup).

If you want to disable automatic cleanup for a specific test, you can use:

import { render } from '@testing-library/react-native/pure';

test('does not cleanup', async () => {
  // This test won't cleanup automatically
  await render(<MyComponent />);
  // ... your test
});

But in most cases, you don't need to worry about cleanup at all - it's handled automatically.

Using get* variants as assertions

Importance: low

getBy* queries throw errors when elements aren't found, so they work as assertions. However, for better error messages, you might want to combine them with explicit matchers:

import { View, Text } from 'react-native';
import { render, screen } from '@testing-library/react-native';

test('uses getBy as assertion', async () => {
  await render(
    <View>
      <Text>Hello</Text>
    </View>
  );

  // ✅ Good - getBy throws if not found, so it's an assertion
  const element = screen.getByText('Hello');
  expect(element).toBeOnTheScreen();

  // ✅ Also good - more explicit
  expect(screen.getByText('Hello')).toBeOnTheScreen();

  // ❌ Bad - redundant assertion
  // const element = screen.getByText('Hello');
  // expect(element).not.toBeNull(); // getBy already throws if null
});

Having multiple assertions in a single waitFor callback

Importance: low

Keep waitFor callbacks focused on a single assertion:

import { View, Text } from 'react-native';
import { render, screen, waitFor } from '@testing-library/react-native';

test('waits with single assertion', async () => {
  const Component = () => {
    const [count, setCount] = React.useState(0);

    React.useEffect(() => {
      setTimeout(() => setCount(1), 100);
    }, []);

    return (
      <View>
        <Text>Count: {count}</Text>
      </View>
    );
  };

  await render(<Component />);

  // ✅ Good - single assertion per waitFor
  await waitFor(() => {
    expect(screen.getByText('Count: 1')).toBeOnTheScreen();
  });

  // If you need multiple assertions, do them after waitFor
  expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1');

  // ❌ Bad - multiple assertions in waitFor
  // await waitFor(() => {
  //   expect(screen.getByText('Count: 1')).toBeOnTheScreen();
  //   expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1');
  // });
});

Using wrapper as the variable name

Importance: low

This is not really a "mistake" per se, but it's a common pattern that can be improved. When you use the wrapper option in render, you might be tempted to name your wrapper component Wrapper:

import { View } from 'react-native';
import { render, screen } from '@testing-library/react-native';

test('renders with wrapper', async () => {
  const Wrapper = ({ children }: { children: React.ReactNode }) => (
    <View testID="wrapper">{children}</View>
  );

  await render(<View testID="content">Content</View>, {
    wrapper: Wrapper,
  });

  expect(screen.getByTestId('content')).toBeOnTheScreen();
});

This works fine, but it's more conventional to name it something more descriptive like ThemeProvider or AllTheProviders (if you're wrapping with multiple providers). This makes it clearer what the wrapper is doing.

Summary

The key principles to remember:

  1. Use the right query - Prefer getByRole as your first choice, use findBy* for async elements, and queryBy* only for checking non-existence
  2. Use proper assertions - Use RNTL's built-in matchers (toBeOnTheScreen(), toBeDisabled(), etc.) instead of asserting on props directly
  3. Handle async operations correctly - Always await render(), renderHook, fireEvent,and userEvent methods
  4. Use waitFor correctly - Avoid side-effects in callbacks, use findBy* instead when possible, and keep callbacks focused
  5. Follow accessibility best practices - Prefer ARIA attributes (role, aria-label) over accessibility* props
  6. Organize code well - Use screen over destructuring, prefer userEvent over fireEvent, and don't use cleanup()

By following these principles, your tests will be more maintainable, accessible, and reliable.

Need React or React Native expertise you can count on?