DEV Community 👩‍💻👨‍💻

Angelos Chalaris
Angelos Chalaris

Posted on

Testing stateful React components the right way

Recently, I was tasked with adding tests to a handful of React components for a project, in order to get familiar with the codebase and the components' usage. What started out as a simple task ended up with a Eureka! moment when a colleague pointed out something I hadn't thought about before.

The specifics of the project and the components are of little to no importance, but the key detail is that I was writing tests for stateful React components that are part of a larger project and edited quite often. My initial approach consisted of writing some basic tests, such as checking if the component is rendered properly and/or if certain events fire appropriately.

However, as part of my tests, I was checking a component's state directly. Now, that is not a sin by anyone's standards, but for a codebase with many moving parts, it is not the greatest idea. Let me show you an example why.

Consider the following test:

context('the component is initialized in a collapsed state', function() {
  let wrapper;
  beforeEach(function(){
    wrapper = mount(<StatefulComponent />);
  });

  it('component state.expanded is false', function() {
    expect(wrapper.state('expanded')).to.be.false;
  });
});

In this example, we check if the component's state has expanded equal to false. Our test will pass, as long as this simple condition is true. It's a very simple test that should be easy to understand even for someone completely unfamiliar with the codebase.

However, over time the component's implementation might change. What happens if expanded in our state ends up meaning something different? Or worse yet, if it isn't reflected the same way in the interface?

Enter the Eureka! moment:

The application's UI should always be considered the result of combining the component's props and state.

What this means is that we should consider the component's state as an abstract concept when testing, much like a neural network's hidden layer, and avoid checking it directly. So, instead of the test presented above, we should be doing something more like this:

context('the component is initialized in a collapsed state', function() {
  let wrapper;
  beforeEach(function(){
    wrapper = mount(<StatefulComponent />);
  });

  it('component does not have the expanded class', function() {
    expect(wrapper.find('div').hasClass('expanded')).to.be.false;
  });
});

Our test is still easy to read and understand, but it's a better test in general.

By checking the DOM directly instead of the component's state, we inform whoever works on this component next what the component should be rendering under specific circumstances, instead of forcing them to use a specific way of doing that. It is a better way to document the component and it's easier to see what has changed from the test itself should someone refactor the UI in such a way that the DOM representation of the component is altered.


Thank you for reading this post.
Feel free to comment below with any feedback or questions.
Follow me for more and share if you think this was an interesting read.

Top comments (0)

Let's Get Hacking

Join the DEV x Linode Hackathon 2022 and use your ingenuity and creativity to build using Linode.

Join the Hackathon <-