Testing React (& Native) Components

React is big. Really big. It is the web framework that you need to be doing right now. It was the Ruby on Rails of 2016. You had to be doing it with a double mocha. And a beard. And a comb-over. Unless you're black. Or a woman. Or sane. Then maybe not a comb-over.

The component based approach of React is really great. The idea of building these small components that do one thing, and do it well, and using these as building blocks of a larger app. Really good principles. Single Responsibility or something like that.

Also, the combination of layout (the JSX) and the JavaScript logic behind it is an interesting approach. At first, I was kinda against it, because we have been indoctrinated into not doing this for a really long time. Yet, I read somewhere that such a separation is superficial, because the JavaScript actually directly affects the layout components, making them part of the same thing. And a rule of thumb is "the things that change together should stay together".

Now, if you have these independent components, that are the building blocks of your application, the question is what do these components do? And by implication, what does one test?

Aside: why test? Because it's good for designing your code in a simple, modular way. It also allows for picking up regression bugs if someone else makes changes and introduces defects, thereby giving a team more confidence to make changes. More confidence means the team can respond to changes quicker, thereby making the team more 'agile' (is that still a thing?).

Whenever one is testing a module (generic trying-to-sound-clever word for a 'thing'), one tests that:

  1. Given an input, or a condition
  2. When I do a certain action
  3. Then what is the output, or result

In a React component, there are 2 inputs (I can think of right now) to a component:

  • Props passed into the component
  • Results from an outside call (e.g. a network call)

There are 2 kinds of actions that can take place:

  • The creation and mounting of the component
  • An event that takes place on one of the child components (e.g. a button is pressed)

Lastly, there are 4 kinds of outputs, or results from a react component:

  • Rendered child components
  • Events that are emitted (e.g. onActionHappened)
  • Network calls are made
  • Messages sent to overarching controlling components, such as a navigator in React Native.

So, when writing a test for the functionality of a component, one would start with the inputs. In these example, I will be using the enzyme library from Airbnb to return a shallow render of the component. This calls the render function of the specified component to test the output, but does not render the child components within the component under test.

import { shallow } from 'enzyme';  
import MyComponent from './my-component';

describe('when I give "Mpho" as a name', () => {  
    const myComponent = shallow(<MyComponent name="Mpho" />)
});

This could also be a result of a network request, in which case you could stub out whichever library you are using for the requests (e.g. fetch-everywhere or xhr), and return a pre-defined result.

From here, one would then go to the action, and then test the result of the action. Such an action could be creation of the component:

describe('when I give "Mpho" as a name', () => {  
    it('should display the name in a text field', () => {
        const myComponent = shallow(<MyComponent name="Mpho" />);
        expect(myComponent.containsMatchingElement(<Text>{'Mpho'}</Text>)).toEqual(true);
    });
});

If the action is an event from a child component, e.g. a button press, the output could be tested as follows:

describe('when I give "Mpho" as a name', () => {  
    it('should display the name in a text field', () => {
        const myComponent = shallow(<MyComponent name="Mpho" />);
        myComponent.find(TouchableOpacity).simulate('press');
        expect(myComponent.containsMatchingElement(<Text>{'Mpho'}</Text>)).toEqual(true);
    });
});

As mentioned earlier, the output could be rendered components, as tested above; or network requests, which could be tested by spying on whichever network library is used; or events emitted, which could be tested by spying on the handler function passed in as properties of the component under test. Sinon is a great library for creating spies and stubs.

import sinon from 'sinon';  
const actionHandler = sinon.spy();  
const myComponent = shallow(<MyComponent name="Mpho" onActionHappened={actionHandler} />);

One of the concerns I had when testing components was that the implementation is closely tied to the tests (i.e. if one changes from a TouchableOpacity to a TouchableHighlight, then the tests have to change). But because, as discussed earlier, the component logic (written in JavaScript) is so closely tied to it's layout (the JSX), so by design, the functionality (logic) cannot and should not be separated from the specific implementation. The rendered layout is a valid external output (and not internal implementation), and if this external output changes, the tests should change to reflect this.

A way in which to avoid closely tying the tests to implementation would be to avoid testing for internal changes and function calls. This includes effects on the state of the object. (I say internal with caution because though these are externally accessible, they are only for internal use). One should also try (as far as possible) to avoid testing for the calling of internal methods of the component. How these methods and state properties are set and called are subject to change. For example, one may decide that they do not want to use state for values that will not change, and set class level variables (e.g. this.myVariable = 'value'). This will not change the output of the component, and therefore tests should still pass unchanged.

This becomes tricky when there is asynchronicity involved as a test normally completes before the asynchronous code has run. In the previous example, if the press action resulted in an asynchronous function call, the assertion would happen before the result of the call. To overcome this, we may need to call a class function as follows:

describe('when I give "Mpho" as a name', (done) => {  
    it('should display the name in a text field', () => {
        const myComponent = shallow(<MyComponent name="Mpho" />);
        myComponent.instance().handleButtonPress().then(() => {
            expect(myComponent.containsMatchingElement(<Text>{'Mpho'}</Text>)).toEqual(true);
            done();
        }); 
    });
});

It's ugly I know. And the test is tied too close to the implementation. If you can think of a better way, please suggest.

A second concern that has been raised is that the tests are too simple, and do not test the wider business logic. I think that's the nature of unit tests. Complementary acceptance and integration tests are needed to have more confidence in the code base. And one may ask, why not have acceptance tests only? And another may answer, because there are a limited number of per-component (or per-unit) scenarios that can be tested with those higher level tests, leaving some parts of the code base untested, reducing the confidence on has in the code base. But many people around the world, a lot smarter than me, are arguing about this. So one can seek their wisdom.

Patrick Kayongo

I create and maintain software. Pan-African.

Johannesburg, South Africa