Testing in React

Paweł Kasperowicz

A few approaches for React Testing. It won’t be focused on the detailed configuration of each tool but it rather show how to embrace tool for better quality of the code.

Introduction

In this text I will describe a few approaches for React Testing. I won’t be focusing on the detailed configuration of each tool, rather show how to embrace tool for better quality of the code.

Flow type

Flow type is only tool described here not used for unit testing. It is useful for static analysis of code. Basically it enhances javascript with adding typing, but it stays only for code analysis, all types are strip out with babel plugin before executing on the browser.

It detects:

  • silent type conversions,
  • null dereferences,
  • and undefined is not a function.

Also with some IDEs it allows to get better autocomplete.

To start using flow: npm install flow-bin -save-dev and touch .flowconfig. After that just add “flow”: “flow” in scripts sections of your package.json file. Finally add to your babel configuration plugin transform-flow-strip-types.

One of the great things about flow is that you can start using it on your current code without any changes to the actual code and types can be introduced just in couple of file by adding // @flow to the file.

Here is a simple example of an annotated function:

function readFile (file: File) : Promise<ArrayBuffer>  {
    return new Promise(function (resolve) {
        let reader = new FileReader();
        reader.onload = e => resolve(e.target.result);
        reader.readAsArrayBuffer(file);
    });
}

The types are followed after colon. To see if there is any error lunch the flow with npm run flow. It does not return any errors. Let’s say we resolve the Promise with the new UInt8Array(e.target.result). After correcting the returned type of the function, flow will check places of usage of that function.

Using the flow with external libraries is not a problem, but requires special effort. Flow allows to write declarations for existing modules. Some of ready to use definitions are available at flow-typed project. For example of such definition look at this repository. In this file we have definition of the buffer library:

declare module "buffer" {
    declare class Buffer {
        constructor(buffer: ArrayBuffer) : this;
        [key:number]: number;
    }
}

Definition can omit parts of library we don’t use. For example class Buffer has much more methods, but they are not used in this app, so I just limited definition to constructor and array operator [].

Defining class data object look like this:

class Image {
    caption: ?string;
    city: ?string;
    country: ?string;
    keywords: string[];
    data: Uint8Array;
}

As you can see there is question sign before string type, which means that this field could be null. It quite useful for detecting null references as quick as possible.

Using flow with react is similar with using PropTypes. The difference is that PropTypes are evaluated at the runtime. Also flow helps checking types of the state. I will focus here on ES6 style defined react components. Here is example of the usage of types in react component:

type Props = {
    onChange: (string) => any
};

class SearchBox extends Component {

    state : {
        value: string
    };

    props : Props;

    constructor(props: Props, context: {}) {
        super(props, context);
        this.state = {
            value: ''
        };
    }

Of course types can be used in any variable or function of the component. Notice that flow will also check passed props by parent components and to the child components. In more complex project, could help to detect harder to find bugs.

Unit testing react components

The first thing we could use is known as react TestUtils. Although it provides basic functionality for react testing, I really recommend using enzyme, which gives easier to use API. It has two approaches of rendering the shallow and the full rendering.

Full rendering

The first thing what, you can do is just render component with enzyme. To render component with enzyme use mount function. It will return enzyme component wrapper.

It requires the DOM. We could use jsdom for it.

import React from 'react';
import { mount } from 'enzyme';
import SearchBox from './SearchBox';

it('calls onChange when input change', () => {
    let onChangeFunction = jest.fn();
    const component = mount(<SearchBox onChange={onChangeFunction} />);

    component.find('input').simulate('change', { target: { value: 'changedInput' }});

    expect(onChangeFunction).toHaveBeenCalledWith('changedInput');
    expect(component.state('value')).toEqual('changedInput');
});

In the example there is a simple demonstration of using the API.  We could assert component state, calls to function, pass props to the component and also whole displayed DOM tree.

Shallow rendering

Shallow rendering is useful for the unit testing single component. Full render will render all child components. Shallow rendering will perform only render in the tested component. In many high level components is no need to render whole tree, just check if logic of the component renders correct set of components and passes to them correct props. Shallow rendering do not require the DOM.

To use shallow rendering just use function shallow. It return enzyme component wrapper. To test interaction between components we could use callbacks passed to the props of the components. Here is the example:

const image = { caption: 'test' };

jest.mock('./Database', () => ({
    getImages: jest.fn(() => Promise.resolve([image]))
}));

beforeEach(() => {
    const Database = require('./Database');
    Database.getImages.mockClear();
});

it('calls getImages when Images component updated', () => {
    const wrapper = shallow(<App />);

    const Database = require('./Database');
    expect(Database.getImages).toHaveBeenCalledTimes(1);

    wrapper.find('Images').props().update();

    expect(Database.getImages).toHaveBeenCalledTimes(2);

});

Database.getImages is called on the App startup and when Images call update. You can lookup sources here.

Designing the components

Right patterns used for the components could be make testing easier. First of all, you could use controller component, which will be maintaining the state and will render only one component which will be the stateless view. In this approach controller component would be just shallow rendered, and we will just test if calling proper callbacks will change the props passed down to the view component. Testing the view component will be just limited to check if it renders valid content and checking if callbacks are called on simulated events.

Encapsulating logic to the pure functions, with single value returned could also make testing easier. With named parameters and flow types functions are easier to read. Functions returning the Promise also helps maintaining side effects. Promise is just an implementation of the monad for asynchronous programming. For synchronous block of code with managing side effects Either monad could be used (for implementation of monads look here).

The last thing is composition (look at this post for many not only compositions examples).

function withAccesCheck(WrappedComponent, type) {
 return class AccessCheck extends Component {
        render () {
            if (User.hasAccesss(type)) {
                return <WrappedComponent {...this.props} />;
            }
            return <div>Illegal access</div>
        }
    };
}

It is simple component wrapper to share common functionality. It is easy to unit test just withAccessCheck. Usage on components will look like this:

export App;
export default withAccessCheck(App);

We could import both wrapped and unwrapped App component. Most cases we would like to test unwrapped component, so we don’t have to test additional functionality each time. Imports will look like this:

import App from './App'; // wrapped component
import {App} from './App'; // unwrapped component

This pattern is used in redux connect. Redux connect allows to put data logic, separated from the component. For article about redux testing look here.

Mocking modules

Jest have functionality for mocking whole module.

const image = { caption: 'test' };

jest.mock('./Database', () => ({
    getImages: jest.fn(() => Promise.resolve([image]))
}));

In the example module Database is mocked with implementation of getImages.

In Jasmine we don’t have such functionality, but we could spy the methods:

spyOn(Database, 'getImages').and.returnValue(Promise.resolve([image]));

Spies will be cleared after running the current it or describe block.

Jsdom

Jsdom is implementation of DOM model for the node.js and it is used in jest. Often for javascript unit testing, the browser such Firefox is used. Why use jsdom then? At first browser adds some overhead causing some poor performance, not needed at unit testing. Other types of tests may require the full browser support. Jsdom supports only for DOM API with no other browser features such as IndexedDB API.

Jsdom could be easily confused with PhnatomJS. PhantomJS is still some kind of limited browser. I does rendering, network operations etc. It still has many overhead, but don’t think of PhantomJS as worst version of jsdom, it still gives you some advantages e.g. network monitoring.

Also jsdom could allow to easily isolate each unit test. To be exactly sure that no other framework has polluted global vars or global state, jsdom can easily create new browser environment from the scratch. Finding such bugs, which come from polluted global state or global vars or even overwritten in other tests prototype could be really hard. Of course it is recommended to avoid such pollution with correct design of the application, but in the real world you have to often manage with current situation.

See also:

Flow vs TypeScript

http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/

http://airbnb.io/enzyme/index.html

http://redux.js.org/docs/recipes/WritingTests.html

https://facebook.github.io/jest

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami