Unit testing React functional component with jest and Enzyme

Introduction

In my current company I am unit testing React components using jest and enzyme. While testing one of the functional components I came across a problem which needed a good solution/explanation and after some research on it I think I found the right way to do it and I wanted to share it with the community

Repo : https://github.com/ovpv/unit-testing-functional-component-enzyme-jest

Good Practise: Write Testcases before defining Component

For the sake of this article, I will be writing test cases for a Login component which will be defined as a standard react functional component. It is a good practice to write the test cases for the component and cover all of it functionalities in them and then start developing it one by one. Initially before developing all your test cases will be failing but as you keep developing your component according to the test cases you have written, your cases will start passing and eventually all of them will pass.

The advantage of such a practise is that you will know 100% how your components work. You can say in confidence that your component is 100% thoroughly unit tested and the code coverage will be 100%.

Before Writing Test cases

I have created a basic react app using create-react-app. We need to download the following dependencies (use –dev flag as these are development dependencies) for this project:

Inside the project I have created a folder path “src/pages/login”. Inside this folder, I will create 2 file

  • login.js
  • login.test.js

login.js will be the main component file and the .test.js will the test file for the same.

Initial component definition for the component will be as follows:

//login.js

import React from 'react';

export const Login = () => {
  return <div>This is Login</div>;
};
//app.js

import React from 'react';
import { Login } from './pages/login';

function App() {
  return (
    <div className="App">
      <Login />
    </div>
  );
}

export default App;

The output looks as follows:

initial output of login component

Before we start writing the test cases , it is important that you take 2-5 minutes to list down the cases you should consider to completely test our component. My list for login component goes as follows:

  • it should render properly (snapshot)
  • it should render an email input tag
  • it should render a password input tag
  • it should render a submit button
  • the default value for both fields should be empty
  • on change of value in the field, the state of that field in the component should be updated
  • on submitting, a submit handler function should be triggered on click event

Well, that’s all I can think of now and this must cover most of the cases for the initial stage. If I have missed any case feel free to comment below!

Writing Test cases

Now, let’s start writing test cases for the cases mentioned above. It’s result should look as follows:

//login.test.js

import { configure, shallow, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import renderer from 'react-test-renderer';
import React from 'react';
import { Login } from './login';

configure({ adapter: new Adapter() }); //enzyme - react 16 hooks support

describe('Login Component', () => {
  it('should render properly', () => {
    const tree = renderer.create(<Login />).toJSON();
    expect(tree).toMatchSnapshot();
  });

  it('should render an email input tag', () => {
    const wrapper = shallow(<Login />);
    expect(wrapper.find('input[name="email"]').exists()).toBe(true);
  });

  it('should render a password input tag', () => {
    const wrapper = shallow(<Login />);
    expect(wrapper.find('input[name="password"]').exists()).toBe(true);
  });

  it('should render a submit button', () => {
    const wrapper = shallow(<Login />);
    expect(wrapper.find('button[name="submit"]').exists()).toBe(true);
  });

  it('the default value for both fields should be empty', () => {
    const wrapper = shallow(<Login />);
    expect(wrapper.find('input[name="email"]').prop('value')).toBe('');
    expect(wrapper.find('input[name="password"]').prop('value')).toBe('');
  });

  it('on change of value in the field, the state of that field in the component should be updated', () => {
    const wrapper = shallow(<Login />);

    /* if the simulated event value and the field value is same then the state is updating on event trigger */

    wrapper.find('input[name="email"]').simulate('change', {
      target: {
        value: 'email@id.com',
      },
    });
    expect(wrapper.find('input[name="email"]').prop('value')).toBe(
      'email@id.com'
    );
    wrapper.find('input[name="password"]').simulate('change', {
      target: {
        value: 'somepassword',
      },
    });
    expect(wrapper.find('input[name="password"]').prop('value')).toBe(
      'somepassword'
    );
  });

  it('on submitting, a submit handler function should be triggered on click event', () => {
    const fn = jest.fn();
    const wrapper = mount(<Login onSubmit={fn} />);
    wrapper.find('button[name="submit"]').simulate('click');
    expect(fn).toHaveBeenCalled();
  });
});

Errors Faced while testing

Before React 16, Enzyme was used to test class based react components and the instance method was used to access the component class’ methods and test it. But with the latest react 16 adaptor, while testing functional components, the instance object is returned as null.

Because of this, I was unable to test the inner methods or state of the components. The methods and state variables/methods were inaccessible as they were inside the scope of that particular function definition.

So, in this case, how are we supposed to test the state updation and inner method of those components. After much research, it became clear that for proper unit testing and getting complete code coverage, it is not recommended to define methods inside the functional component.

In order to keep the methods unit-testable, it has to be defined outside the functional component and exported along with the component. That way all methods defined can be imported into test file and properly unit tested.

In the functional approach, we need to use the useState method to define and update the state of the component. How are we suppose to test it as it cannot be exported like the fix given methods inside functional components??

Well, the truth is we directly cannot test the state change of the state is not directly accessible to test as it is inside the functional scope. Instead, we will have to test the side-effects caused by the state update.

In our project, we are trying to update the state of email/password field when the on-change event triggers the field. To test the state change, we are simulating the change event and passing the value for event.target.value and then we are comparing if the fields prop “value” is equal to the value given to the simulated event target. If it is equal that means the component state is updating on event change successfully. See the login.test.js file for more clarity.

Defining the Login Component

As per the test cases defined and considering the errors mentioned above, we can define our login functional component as follows:

//login.js

import React, { useState } from 'react';

export const Login = ({ onSubmit }) => {
  const [email, updateEmail] = useState('');
  const [password, updatePassword] = useState('');
  const handleLogin = (e) => {
    e.preventDefault();
    //do all the default login activities and then call the onsubmit prop method
    onSubmit();
  };
  return (
    <div>
      <input
        type="text"
        value={email}
        name="email"
        onChange={(e) => updateEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        name="password"
        onChange={(e) => updatePassword(e.target.value)}
      />
      <button type="submit" name="submit" onClick={(e) => handleLogin(e)}>
        Login
      </button>
    </div>
  );
};

The results

Once the component is being developed step by step it keeps satisfy the previously failed test cases and now you will see that it starts passing. When the component definition is complete, All your test cases should be passed and you should have 100% code coverage as shown below:

all tests passing and 100%code coverage

Conclusion

Nothing makes a developer happier than seeing all the test cases for an app pass along with having a 100% code coverage. It sure is challenging but not impossible. Writing test cases might consume more time but it ensures good code quality and very stable codebase. I would always recommend every developer to follow this pattern and you will rarely worry about any bugs in your code.

I hope you guys enjoyed these articles. Let me know your views/suggestions in the comment section below.

26
26