codete testing front end apps with testing library main 03b5315869
Codete Blog

Testing Front-end Apps with Testing Library

Krzysztof Bezrak ea37a034ca

28/06/2021 |

10 min read

Krzysztof Bezrąk

Testing can be hard, especially on the frontend. You’ve probably heard the term test pyramid. In a nutshell, it’s a concept that encourages developers to write more unit tests (because they run fast, but are testing a single unit in isolation), and less integration or E2E tests (as they are slow, but can test integrations between different parts of a system).

(source: https://martinfowler.com/articles/practical-test-pyramid.html)

In the past, it was quite common to only test frontend with E2E tests. In many codebases, it was hard to apply other testing techniques, because of the separation of HTML, CSS, and JavaScript. 

Thankfully, nowadays we usually work with component-oriented libraries and frameworks — be it React, Angular, Vue or Svelte. Even with many fundamental differences between them, I think we can agree that they have one thing in common — they create a DOM tree from nested components, every of which can receive data from the parent component using props, having some form of private internal state as well. These components are composed of HTML markup and JavaScript logic (and often styling as well). We can treat them like coupled units, which are reusable and self-contained. Thanks to this, we gain the possibility of unit testing frontend in terms of such components.

 

Shallow rendering

For unit testing of such components, there is a popular technique of shallowly rendering them, using libraries such as Enzyme. But what exactly is shallow rendering? It’s a technique for rendering components only one level deep down in the component tree. For example, let’s take a look at the below React component:

import React from 'react';
import Main from './components/main';
 
const Menu = ({ theme, toggleTheme }) => (
<ul>
  <li><a href="/">Home</a></li>
  <li><a href="/login">Login</a></li>
  <li>
   <span>Theme: {theme}</span>
   <button type="button" onClick={toggleTheme}>Toggle theme</button>
  </li>
</ul>
);
 
export default class App extends React.Component {
 state = {
   theme: 'light',
 };
 
 toggleTheme = () => {
   this.setState(({ theme }) => ({
     theme: theme === 'dark' ? 'light' : 'dark',
   }));
 };
 
 render() {
   const { theme } = this.state;
 
   return (
     <div>
       <Menu theme={theme} toggleTheme={this.toggleTheme} />
 
       <h1>Title</h1>
 
       <Main theme={theme} />
     </div>
   );
 }
}

In the case of shallow rendering, <App /> would not render list contents from <Menu /> component, or whatever we do have in <Main/ >. It will only represent those components as elements with correct props passed. If we want to test any logic inside the component, we can use – for example – the instance method in Enzyme. Tests for this component can look like this:

import { shallow } from 'enzyme';
import App from './App';
 
test('sets app initial theme to light', () => {
 const wrapper = shallow(<App />);
 
 expect(wrapper.state().theme).toBe('light');
 expect(wrapper.find('Main').prop('theme')).toBe('light');
});
 
test('toggles theme of the app', () => {
 const wrapper = shallow(<App />);
 
 wrapper.instance().toggleTheme();
 
 expect(wrapper.state().theme).toBe('dark');
 expect(wrapper.find('Main').prop('theme')).toBe('dark');
});

Calling shallow method with the <App /> component will result in rendering something like this (you can check it by logging wrapper.debug()):

<div>
 <Menu theme="dark" toggleTheme={[Function (anonymous)]} />
 <h1>
   Title
 </h1>
 <Main theme="dark" />
</div>

It's a good example of unit testing — we only focus on testing one component at a time, and we don't care about other components. We can just make sure that we pass correct props to them, and we can test how these components react to given props in their own tests. And, what I find really convenient, we don't have to mock all dependent components in every test . Just imagine mocks in test for a component made up of 10 different ones! Also, shallow testing is fast (not only in terms of running, but in terms of time needed to write them as well), which is what we expect from unit tests.

But is unit testing a good approach for UI components? After all, they are meant to be composed of other components, and shallow rendering can hide potential problems caused by rendering children. Notice as well that this approach works only with class components, because we can't access instance of a function component (so we can't test a similar component using useState hook). Also, tests like these two above focus and rely heavily on implementation details — should it really matter if we store the theme variable in the local state of the App component, or if it's provided by React Context, or maybe if it's even a part of a MobX store? Any refactoring done to this part will result in changes for the tests as well. And it's also worth noting that we only test the method which causes component state change, resulting in props change of another component — but we don't test any real outcome of these changes, nor do we test how this action would be triggered by a real user.

But what if we can combine speed benefits of shallow rendering with confidence given by integration tests? Let me introduce the Testing Library, which does exactly this!

 

Testing Library

But why do we write tests? For me, answering this question was crucial for understanding concepts that we will cover later in this article. So, the answer that comes to my mind is that we write tests to be confident that our application works correctly for our users. Not only in the moment of writing tests, but also on refactorings, when adding new functionalities, and so on.

Testing Library philosophy can be summed up with this quote from Kent C. Dodds (author of the library):

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

Because of this, tests written using the Testing Library don't focus solely on the implementation part (like the tests above), but they try to operate with components just like the end user of our application would do. To achieve that, it works with DOM nodes instead of component instances. It's quite fast — maybe not as fast as shallow rendering, but still not slower in an order of a magnitude. What's in my opinion more important though, is that you shouldn't notice a difference when writing tests by yourself, which is great for development experience (BTW, Jest has an ability to only run tests for files that you've locally changed, so even if test execution time increases with test count, you don't notice it much in your daily development, which is super cool!).

Okay, enough of the introduction, let's see a real test case of a similar component:

import { render, screen, fireEvent } from '@testing-library/react';
import App from './App';
 
test('sets app initial theme to light', () => {
 render(<App />);
 
 expect(screen.getByText('Theme: light')).toBeInTheDocument();
});
 
test('toggles theme of the app', async () => {
 render(<App />);
 
 fireEvent.click(screen.getByRole('button'));
 
 expect(screen.getByText('Theme: dark')).toBeInTheDocument();
});

What we can notice at the first glance is that our test now looks more like an end-to-end test. We search for a node containing given text in the DOM tree or do a button click and look for the following changes, but still we don't need to bother about all the E2E setup (no need to run a full app, bundler, browser or a complex test framework). And we only test a specific component in isolation from its parent, but with its own child component dependencies! In this example, we rely on children rendered from the <Menu /> component to test the logic in the App component, but we can also test for something in the <Main /> component.

Testing Library selectors need some getting used to. They behave differently when a component does not exist, if there's only one or many occurrences, and so on. You can read more about them here. What I find really nice about them is the fact that they enforce writing accessible websites. You are not able to obtain an element by a selector-like query, which is what most E2E testing frameworks use. Instead, you should query elements just like the end user would do — by the accessibility role, label, text, or in the worst case scenario by test-id attribute. When adhering to those guidelines, we can ensure that our components are not only testable, but also accessible to people using screen readers or other assistive technologies. Neat!

Notice how much easier it is to read those tests, even for a non-technical person. There's no mention of state, component instance, or anything related to the framework we are using (besides @testing-library/react package of course). We can easily refactor our component to be a functional component using useState hook without changing a single line of our test. Heck, at this point we can even try rewriting our app in Vue or Svelte and use this test as a reference with subtle changes to the render method here and importing another @testing-library package.

 

How to start writing tests with Testing Library

It mostly depends on what UI library or framework you use. The Testing Library offers bindings for most popular ones, and even for a few not-so-trendy as well, so there's a huge chance that you can introduce it easily to your project. It also requires a test runner with JSDOM configured — Jest is the most popular choice as it comes with it out of the box (and it's recommended by library maintainers), but any other library can be configured to use it as well (such as Mocha).

In terms of React, if you've bootstrapped your application using create-react-app, here's some good news for you — Testing Library is included by default in new projects, and it's even mentioned as a recommended tool for writing tests in the official React docs. You can just yarn create react-app [name here] and start writing your tests! If you maintain tooling for your React app by yourself, then it's only a matter of installing @testing-library/react as a dev dependency and importing utilities it provides in your tests. There's also a react-hooks-testing-library package, which can help you to write tests for large and complex hooks.

But it's definitely not limited to React only. You can use Testing Library with Angular, Vue or Svelte just to name a few, but there are even more integrations described in the docs.

For even more convenience, you can try the @testing-library/jest-dom package, which adds a few custom Jest matchers, so you can use expect(...).toBeInTheDocument() like in the example above. It's enabled by default in create-react-app as well.

 

Conclusion

You can clearly see that the Testing Library provides developers with an easy way to write tests that focus on ensuring that the application will work correctly when our users will use it. I consider it to be a better replacement for the traditional, shallow rendering-based approach to testing that was popular back in the early React days, but without sacrificing its speed benefits.

But, while I find Testing Library convenient for testing components, I still write typical unit tests for utility functions or state management objects (like MobX or Vuex). Sometimes advanced logic they contain is better to be tested outside of a component, because with component tests it would be too complex to simulate all possible changes in the external store, when operating with a system under test just from the DOM perspective.

What is your approach to testing modern JavaScript apps? Did you use the Testing Library in any of your projects, and if so, how do you like it? Or maybe you have your own reasons to prefer shallow rendering? Let us know in the comment section!

Rated: 5.0 / 1 opinions
Krzysztof Bezrak ea37a034ca

Krzysztof Bezrąk

Frontend Developer who mostly works with React but likes to delve into backend, mobile, and DevOps topics from time to time. He likes to „hack” any hardware that surrounds him. In his free time, he loves to travel and enjoy good food and beer wherever he happens to be.

Our mission is to accelerate your growth through technology

Contact us

Codete Global
Spółka z ograniczoną odpowiedzialnością

Na Zjeździe 11
30-527 Kraków

NIP (VAT-ID): PL6762460401
REGON: 122745429
KRS: 0000983688

Get in Touch
  • icon facebook
  • icon linkedin
  • icon instagram
  • icon youtube
Offices
  • Kraków

    Na Zjeździe 11
    30-527 Kraków
    Poland

  • Lublin

    Wojciechowska 7E
    20-704 Lublin
    Poland

  • Berlin

    Bouchéstraße 12
    12435 Berlin
    Germany