In this article I wanted to explain how I used Jest and Enzyme to test a simple component that I use in my application. Jest is a front end testing framework that contains a number of features to allow me to successfully test the correctness of my application. I choose Jest because of its simple configuration and its rich feature set including mocking, easy finding of tests (just place them in __tests__ folder or .test.js and Jest will run them). Jest code coverage (based on Istanbul) is also simple to set up and run. Jest is also authored by Facebook which gave me some confidence of the frameworks documentation. Others may disagree but I have found all documentation by the Facebook development community very well done and concise. The other important utility in my toolchain is Enzyme. Enzyme is authored by Airbnb and was specfically written to make testing React Component output easier. The combination of Jest and Enzyme gives me the ability to:

  • Test properties being passed to components;
  • Simulate form events (click, blur, submit etc);
  • Simulate errors;
  • Determine state values before and after an event happens;
  • Verify the DOM output of a component;
  • Verify behaviour (i.e. Has something you expect happened).

What are we testing?

We will be testing the SignUpForm controlled component in my application. As the name suggests this is a simple form to register a user onto the platform. As with every form there is some logic and error conditions. Render() will be executed whenever there is an update to State. An early version of the render method for the SignUpForm looks like the following and we will use this for the basis of this article:

///...

 render() {
    const {
      username,
      email,
      passwordOne,
      passwordTwo,
      submitError,
      isChecking,
      touched
    } = this.state;


    const isInvalid =
      passwordOne === '' ||
      passwordTwo === '' ||
      email === '' ||
      username === ''

    const formErrors = this.validateForm();
    
    return (
      <React.Fragment>
        <form onSubmit    ={this.onSubmit} className="ui form">
          <div className="field">
          <label htmlFor="userName">Lets get started by choosing your unique name (Max 15 Characters) </label>
            <input
              id="userName"
              value={username}
              onChange    ={event => this.setState({username: event.target.value,userNameTaken:false,touched:{...touched,userNameField:true}})}
              type="text"
              maxLength="15"
              placeholder="Username"
              onBlur    ={this.checkUserName}
            />
            {isChecking && <div className='ui active mini inline loader' />}
            {formErrors.userNameErrors && <div id="userNameError" className='ui red pointing basic label'>{formErrors.userNameErrors}</div>}
          
          </div>
          <div className="field">
          <label htmlFor="email">Now, enter your email address</label>
          <input
            id="email"
            value={email}
            onChange    ={event => this.setState({email: event.target.value,touched:{...touched,emailField:true}})}
            type="text"
            placeholder="Email Address"
          />
          {formErrors.emailErrors && <div id="emailError" className='ui red pointing basic label'>{formErrors.emailErrors}</div>}
          </div>
          <div className="field">
          <label htmlFor="passwordOne">Choose your secure password</label>
          <input
            id="passwordOne"
            value={passwordOne}
            onChange    ={event => this.setState({passwordOne: event.target.value,touched:{...touched,passwordOne:true}})}
            type="password"
            placeholder="Password"
          />
          </div>
          <div className="field">
          <label htmlFor="passwordTwo">Finally, confirm your password</label>
          <input
            id="passwordTwo"
            value={passwordTwo}
            onChange    ={event => this.setState({passwordTwo: event.target.value,touched:{...touched,passwordTwo:true}})}
            type="password"
            placeholder="Confirm Password"
          />
          {formErrors.passwordErrors && <div id="passwordError" className='ui red pointing basic label'>{formErrors.passwordErrors}</div>}
          </div>
          <button disabled={isInvalid || formErrors.hasError} type="submit">
            Create your account
        </button>
          {submitError && <p id="submitError">{submitError}</p>}
        </form>
        </React.Fragment>
    );
  }

What can we take away from this form:

  • Each time the render method is run validateForm is called;
  • There is a username field that onChange sets a state variable;
  • The username field has a blur event that checks the user name;
  • A spinner is displayed when a variable called isChecking is true;
  • PasswordOne field that sets a state variable onChange;
  • PasswordTwo field that sets a state variable onChange;
  • Submit button that is disabled if isInvalid is true or there are errors on the form;
  • userNameError section to display errors with the user name;
  • passwordError section to display errors with the user name.

In addition to the tests we can determine from looking at the code snippet above, there are some business rules around the form as well. For example, a username must be unique, a username cannot contain spaces or non-alphanumeric characters, email address must be valid. So the tests we can look at are:

  • Test the form is validated on render method being run;
  • Test that the input fields are blank by default;
  • Test the submit button is disabled by default;
  • Test that an error is displayed if the Password and Confirm Password fields do not match;
  • Test the service method is called when checking for existing username;
  • Test the service method to register a user is called correctly;
  • Test the component can handle gracefully should a call to a service fail.

There are more tests we can execute, but this is a good start for a suite of tests that can give us some reassurance that our code is behaving as expected and also gives us confidence that should we change anything we are confident the code runs as we designed it should. Although the form is simple we can already see a number of logical events that we can test and more excitingly we can leverage Jest and Enzyme to help us out achieve these tests. The rest of the article will show examples for each of these tests with associated snippets.

Testing form validates on render()

The following test relies on knowing the lifecycle of react and when the render method is called. The render method is part of the React lifecycle and is trigged once the component is mounted during the initialisation phase and when there are changes to the State and prop changes.

it('Sign Up Form validate is called when text is entered ', done => {
        const userName = 'userA';
        const spy = jest.spyOn(SignUpForm.prototype, 'validateForm');
        
        let communityFormWrapper = mount(<MemoryRouter><SignUpForm history={history} /></MemoryRouter>);
        communityFormWrapper.find('#userName').simulate('change', {target: {value: userName}});
        
        setTimeout(function() {
            expect(spy).toBeCalledTimes(2);
            done();
        },100); 
    });

Jest gives us the functionality to set up a spy to track the object and return a mock that we can evaulate. Enzyme is used to mount SignUpForm. Using mount allows the simulation of the component going through a full mount lifecycle. Once the form is mounted we can use Jest to simulate a change event that will we know will case the State to change trigger a render. The expectation allows us to verify that the validateForm method is called twice - once during mount, and once during the State change.

Test that the input fields are blank by default

To test the input fields are blank we don't need access to the full lifecycle and shallow is recommended to be used as it prevents accidently testing child components. However because the component is wrapped with a MemoryRouter I need to grab an instance of the shallow rendered component and dive to be able to access the props of the component. We can examine this instance and verify the value prop is empty. This test can be repeated to test the individual form fields.

it('Sign Up Form email is blank by default', () => {
        let signUpFormWrapper = shallow(<MemoryRouter><SignUpForm history={history} /></MemoryRouter>);
        let signUpFormInstance = signUpFormWrapper.find(SignUpForm).dive();
        expect(signUpFormInstance.find('#email').props().value).toEqual('');
    });

Test the submit button is disabled by default

We can use the same method as above to check the status of the submit button to check it is disabled by default. This should be the case as the form is not yet populated.

it('Sign Up Form button is disabled by default', () => {
        let signUpFormWrapper = shallow(<MemoryRouter><SignUpForm history={history} /></MemoryRouter>);
        let signUpFormInstance = signUpFormWrapper.find(SignUpForm).dive();
        expect(signUpFormInstance.find('button').props().disabled).toBe(true);
    });

Test that an error is displayed if the Password fields do not match

A common error message for a registration form is to verify that both the password field and the confirmation password match. In this scenario the button should remain disabled since the form contains errors.

 it('SignUp Form submit button is disabled when passwords do not match ', () => {       
        const userName = "userA";
        const email = "[email protected]";
        const passwordOne = 'password';
        const passwordTwo = 'passw0rd';
        let signUpFormWrapper = shallow(<MemoryRouter><SignUpForm history={history} /></MemoryRouter>);
        let signUpFormInstance = signUpFormWrapper.find(SignUpForm).dive();

        signUpFormInstance.find('#userName').simulate('change', {target: {value: userName}});
        signUpFormInstance.find('#email').simulate('change', {target: {value: email}});
        signUpFormInstance.find('#passwordOne').simulate('change', {target: {value: passwordOne}});
        signUpFormInstance.find('#passwordTwo').simulate('change', {target: {value: passwordTwo}});
  
        expect(signUpFormInstance.find('#passwordError').length).toBe(1);
        expect(signUpFormInstance.find('#passwordError').text()).toEqual('Passwords do not match');
        expect(signUpFormInstance.find('button').props().disabled).toBe(true);
    });

Again, we use Jest and Enzyme to mount and simulate filling the form in. The username and email are entered correctly but the passwords do not match. I believe that a test should test one thing but for this example I have tested both the passwordError field and the submit button disabled status. This test ensures that should passwords not match, the expected error message is displayed with the expected message and that the button is disabled. If we were to update passwordTwo to match passwordOne this test would fail.

Test the service method to register a user is called correctly

There is an argument that when testing React components we should test the output of the component rather than implementation details. This test however tests an implementation detail in that we are testing to see if a call to the service call is made successfully. I have included this to show how we can do this. One of the rules are that usernames must be unique, contain no spaces or non-alphanumeric characters. To enforce this a service call is made to query the database to ensure the username does not exist. The component code executes this code once the user moves away from the field using onBlur. onBlur can be simulated by Jest.

it('SignUp Form doCheckUserNameExists method is called once user moves away from field (onBlur) ', done => {
        const userName = 'userA';
        
        let signUpFormWrapper = mount(<MemoryRouter><SignUpForm history={history} /></MemoryRouter>);

        signUpFormWrapper.find('#userName').simulate('change', {target: {value: userName}});
        signUpFormWrapper.find('#userName').simulate('blur');
        
        setTimeout(function() {
            expect(FireStoreService.doCheckUserNameExists.mock.calls.length).toBe(1);
            expect(FireStoreService.doCheckUserNameExists).toHaveBeenCalledWith(userName.toLowerCase());
            done();
        },100); 
    });

Once we have simulated blur we can check our FireStoreService mock to test. In order for the test to work we need to create a manual mock for and declare it. The manual mock allows us to mock out the FireStore service and return fake data or results. The manual mock we create must be placed in a __mocks__ subdirectory adacent to the module under test. In our case the structure looks like:

services 
	|-  
    	__mocks__
    		|- fireStoreService.js 
    |- firestoreService.js

Once we configure the test to mock FirestoreService it will use the mocked version located in the __mocks__ subdirectory. To configure it we simply call:

jest.mock('js/services/fireStoreService');

With the structure in place we can now write mock implementations for the methods we need to test. The mock implementation for doCheckUserNameExists simply returns a resolved promise.

With everything in place the test can be executed successfully and we can verify that the service is called successfully but also that it was called with the correct parameters.

Test the component can handle gracefully should a call to a service fail

The final example for this article tests the component to ensure that it handles and informs the user when a call to a service fails.

it('SignUp Form doCheckUserNameExists handles error correctly ', done => {
        const userName = 'userA';
        const dummyError = new Error("Test Error Message");
        jest.spyOn(FireStoreService, "doCheckUserNameExists").mockImplementation(()=>{throw dummyError});

        let signUpFormWrapper = shallow(<MemoryRouter><SignUpForm history={history} /></MemoryRouter>);
        let signUpFormInstance = signUpFormWrapper.find(SignUpForm).dive();

        signUpFormInstance.find('#userName').simulate('change', {target: {value: userName}});
        signUpFormInstance.find('#userName').simulate('blur');
        
        setTimeout(function() {
            expect(FireStoreService.doCheckUserNameExists).toThrow(dummyError);
            expect(signUpFormInstance.find('#submitError').text()).toEqual(dummyError.message);
            done();
        },100); 
    });

We use Jest to Spy on the doCheckUserNameExists on the FireStoreService and override the manul implemtnation using mockImplementation. Instead of the manual implementation we throw an Error. As we did previously lets simulate blur to execute the doCheckUserNameExists and then verify what happens. The test assets that the error was thrown but we also verify the error message is displayed to the user.

Wrapping up

Thank you for reading the article and hopefully you have seen how to use Jest and Enzyme can be used to write tests for React components. We have used a number of features from Jest and Enzyme including Spys, Mocks, simulating DOM events, verifying behaviour and what the output of the component is after certain scenarios have played out. I find working with Jest and Enzyme an enjoyable experience and gives me confidence in my code and the ability to refactor.

Further documentation on these tools can be found on their own websites.