Become a Testing Library expert using the Testing Playground

If you are using react and test your components (you should!), then you are probably using the Testing Library. Testing helps to be confident that your components work as expected — especially in corner cases that are harder to test manually. The learning curve for a library can be steep, but doesn’t have to!

Testing Library provides a set of tools for testing your components, following best practices such as avoiding to test implementation details. If you test implementation details you are tightly coupling test and implementation, you might have to change tests every time you refactor your component. This is easier said than done 😆 — You need to know how to utilize to potential of the library. You often have a strong tool but don’t know how to reach its full potential (the good old “a fool with a tool is still a fool”).

The Testing Library is lightweight, but still has a lot of features — which isn’t bad at all. It’s designed to test components for semantics, not their implementation. That means to test the way the user interacts with the components. It provides a lot of good documentation and guidance. For example, it explains how to interact with the elements in the component, to be as close to how real users interact with them. You can use different kinds of selectors to query the elements, which have different priorities for queries that test implementation to queries that test semantics.

I would consider myself as an intermediate user of the library, as I already used a lot of its features. However, I don’t know all the tricks around queries yet. Let’s see how we can improve that. This example component from this blog, but a bit simplified:

export const PostCard = ({ post }: { post: Post }): ReactElement => {
  return (
    <Card sx={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
      <ButtonBase
        component={Link}
        href={{
          pathname: '/posts/[year]/[month]/[name]',
          query: { ...post.slug },
        }}
      >
        <CardMedia />
      </ButtonBase>
      <CardContent sx={{ flex: 1 }}>
        <Typography gutterBottom variant="h5" component="h3">
          <Link
            href={{
              pathname: '/posts/[year]/[month]/[name]',
              query: { ...post.slug },
            }}
          >
            {post.title}
          </Link>
        </Typography>
        <Typography variant="subtitle2" color="text.secondary">
          {post.date}
        </Typography>
        <Typography gutterBottom variant="body1" component="p">
          {post.excerpt}
        </Typography>
      </CardContent>
      <CardActions>
        <AuthorLinks names={post.authors} />
      </CardActions>
    </Card>
  );
};

Let’s write a test for it. What do we want to test? That the post title is displayed? That the author name is displayed? That the link actually goes to the post page?

How about querying the <a> element? Maybe not the best idea, as the tag is an implementation detail. We need to concentrate more on the semantic. Let’s query for the link role instead — but there are multiple links in the output… So we need to know whether the link is the correct one.

For the start, we should have a look at the HTML generated by the component. That way we know what we can query for. As you are probably running your tests headless, you can’t just fire up the Chrome Dev Tools to explore the DOM tree, neither can you use the browser extension. Testing library provides some helpers here: screen.debug() outputs the tree as HTML. This is similar to the output that is printed if a test query fails. If you struggle to find the perfect query, try the screen.logTestingPlaygroundURL() method.

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

describe('PostCard', () => {
  it('should render without exploding', async () => {
    const author: Author = {};
    const post: Post = {};
    render(
      <AuthorsContextProvider authors={[author]}>
        <PostCard post={post} />
      </AuthorsContextProvider>
    );

    screen.debug();
    screen.logTestingPlaygroundURL();
  });
});

It outputs a link to the console that brings you to the Testing Playground — An interactive playground to discover the best selectors to use.

  console.log
    Open this URL in your browser

    https://testing-playground.com/#markup=DwEwlgbgfKkAQGMA2BDAzmgvAIgLIFcwAFFABwFMAnAWkoHs6AXOA4sq68pciFRsOgDsWhEhRr18gkORAi24zt178hARnkBhFJRC0GzBBmprGCABYAGAOwAzaqzEd6TB4W279TbHBB8U1IzkaPwgOKR0IdQIOiDYMCiIqBg4rAAqAJ4RAOaUZOYZXszpWXS5+YVgguZUYMWEADJVANZF8k2CrVIylEhV5ACCSADuKBlo8gBC+IyMQpPo5G1GaCYQ+ACc1hsAzG5gmTl5pAVF+x2tLoz707Pzi0U+jCgARlUyAB44lj7mlOS2HAAegiITQQIATJYIWogZZYaDrvxGNx4rBoMAgeAMWhSChhMh0Fg8IQ0nR8BYAEpgUikbjLYwADgAVuRGQAvcz7MkU8zU2n0q5ooG4-EwIEoGDYpJE1LuWKaIRBQTXK6IYxbN7ZQT7DwgRUq8gqx4wcw7GUpEkHUrlE6FNUlI4VajmACs8kOZWOp2yMyClEmTDmAFt1atzLskMzWo6vc6hQkLcTY7bTg7STbvZVqrV6mALm1WAXulQ+oJBiMxhMViYwMNGOXrNzM-GDOcWo84H8AcDEeCoTC4QjIki6qioEQR3A0mPyJjJZizTB+MHyL4+OQV+QcAO1NR4fuIWlLJYAFwn8+WAB0J8sAC14gApfH4HQZOAQgA0H+hakxW5gUgkzla0nTtQsMzA04XjoEAMg0FMs2oX1ZioQM7lDGs1A+YYwHMNQ6GbKD7QMeIAAkuCQOg4AAdToXoQExUhxWxKV4EJS1WD1AYEFUQRVnTMBuN4gR+OoUUECqbIwxMRgADZyA2ZkIF1WIeL4gTSLYiBgKtQMPgZVZrEsABHCBmQ2NFpQ45NCAGFQdAAcUkUhDJMDJGHwWx2X2ez-EoZzyVchN0V01g-OeCQDHkCKdH0Es5HChyaAQOgqMoAARAEUHwJBDGMRhzBACz7CS-yTWANAIGkmyQIAZWqgBJVKdUEhrsmaoRqFsJU6rAdlyFwWQwHwUMysi7qUCQJAXhQBBmhktRLGDMBGSiVh2s61q23GuLbCmma5uaHweoQfA0FebgcH2pA0HIHwIDAchhn0744EsD8ABYvp8HQwACcwwBAGRBBwRhKHwe712eQJglCHAiCoNAhC2tE8UK3xUjUCE4GxhAISvGF3rgT6TCvbYSeoT7VjUcmNiphnSe+2ntipkncbp9nvs+9lgw+iEEGoAm5Osd7qEZDmdlJiXPogCF8LklSBcsIWrzkuTqFdK8dj2aWqfZYV0fMcUquyFjIHNjEgNqq1PVTEimA9FtwLQfAXmRbgcawlATLUaxSEKRDW28GBRWEPwYaCEIgZwHLCvo6gy2OxMbaD8DBLtpCqhqSg6naDtBOLaRS36IZRnGRbmmyWxyGyPQ07THbGgL0iu3+QFsAlGZzHo8EAHk+ggKgAFIoTq-E4igAfICoOBx+kecTbxQRxWYzFWPXi3N4xLELaAA

Click on the link to see your example in the Testing Playground. You can also copy the output of screen.debug() and paste it to the top left of the Testing Playground manually.

Testing Playground
Testing Playground allows to discover the best queries for your tests.

The playground suggests querying by role, but with an additional name filter! While I knew the getByRole() query before, I didn’t knew you could pass a name filter – which is nice to learn. The playground takes care of the query priority and displays all suggestions at the bottom right. You can choose between them. If it can’t find a good query, it will still suggest a basic one, while warning that it’s tightly coupled to the implementation.

expect(
  screen.getByRole('link', {
    name: /post title/i,
  })
).toHaveAttribute('href', '/posts/2021/01/post-title');
expect(screen.getByText(/january 2, 2021/i)).toBeInTheDocument();
expect(screen.getByText(/hello world/i)).toBeInTheDocument();
expect(
  screen.getByRole('link', {
    name: /oliver sand/i,
  })
).toBeInTheDocument();

I think the Testing Playground is nice if you are new to the library. It helps to follow best practices by testing semantics, not implementation. You will probably not always use it, especially once you are more familiar with the library. I would like to see more libraries providing such companion tools. It’s a good improvement to developer experience, providing a nice learning experience, and helps to get from intermediate to expert quickly.