This website uses cookies to ensure you get the best possible experience. See our Cookies Policy.

PMG Digital Made for Humans

Testing React

13 MINUTE READ | March 12, 2019

Testing React

I was recently assigned a ticket in which I had to rebuild a UI in React. Here at PMG, we use Karma, Enzyme, and Chai to help test our React components. In completing the ticket, I learned a few things about Mocha/Enzyme and thought I would share.

Let’s start with a quick tip in the Mocha testing framework.

While I was working on some components, and running our nearly 700 tests, I thought that it might be a little faster if I could only run the current tests that I was working on. Turns out in Mocha, this is pretty easy.

You normally have a test like this:

basic_test.js

describe('Something about the tests here', function () {    it('tests one thing', function () {    });});

And if you want only this file of tests to run, simply need to add .only to the describe, like this:

only_example.js

describe.only('Something about the tests here', function () {    it('tests one thing', function () {    });});

This is not a big deal, saves just a little time, but I think it keeps the output focused on the tests you are interested in, so I thought I would mention it.

One thing, if you use something for Continuous Integration, like Travis…Be sure to remove this .only before you push your code, otherwise your CI will not be running all your tests.

Additionally, you can .only to the it() call like it.only() in order to limit to a single test, but that seems a little overkill to me. Now let’s move onto to React/Enzyme tips.

The main focus of this article will be Enzyme as the ticket I had to complete was mostly UI related and used a lot of React components, which Enzyme is intended to make testing those components easier. I implemented the required changes, creating new components, creating new routes, and all the other little things. Enzyme provides some helpful functions that allow you to simulate rendering your React components, the mount(), shallow() and render() functions.

Let’s quickly describe the difference between mount(), shallow(), and render(). mount() is a full DOM rendering of the component, including the children. In order to do this, you will need to run in an environment that is like a browser. This can be done with a headless browser of some type, or jsdom or similar. The benefit here is that you will have access to the full component as if it were rendered on a page. shallow() is a great way to limit what is being tested. It only renders the parent component and ignores rendering any child components. This is good so that you don’t end up writing tests for child components in your tests, as those should be tested individually. It is pretty common to use shallow() more often than mount(). render() is used to render components, but it uses a third party HTML parsing/traversal library called Cheerio. You will get a CheerioWrapper with render(), which is similar to mount()‘s ReactWrapper and shallow()‘s ShallowWrapper. But is important to be aware of this difference as the wrapper has a different API than the other two. Another thing to be aware of with render() is that it is a “static” render, meaning the lifecycle calls are not made, and event handling is not available. Often render() may be a preferred choice for “dumb” components, which are components that are ignorant of state, and are presentational in nature. For illustration purposes, let’s look at a few simple components and how we might go about testing them. Let’s create a Foo component, that has a Bar component as a child.Foo.js

import React from 'react';import { Bar } from './Bar';export function Foo() {    return <div>        <div className="fooClass">            <Bar />        </div>    </div>;}

The Bar component:

Bar.js

import React from 'react';export function Bar() {    return <div>        <div className="barClass">Something here</div>    </div>;}

Now let’s write a simple test for Foo just to get things rolling:

Foo.spec.js

import React from 'react';import { render, mount, shallow } from 'enzyme';import { Foo } from 'Foo';describe('Test for Foo component', function () {    it('can mount Foo component', function () {        const wrap = shallow(            <Foo />        );        expect(wrap.find('.fooClass')).to.have.length(1);    });});

In this test, we are using shallow() since in these tests we don’t really care what Bar does. This will return an instance of ShallowWrapper (if using mount() you get a ReactWrapperWrapper API. When you have an instance of either ShallowWrapper or ReactWrapper you can call .debug() on the wrapper, to see what is “inside” the Wrapper. So in our first test if we add console.log(wrap.debug()) when we run our tests, we should see the output of the Wrapper. If shallow() mounted it should look like this:

shallow_example.html

<!-- shallow() -->
<div>
  <div className="fooClass">
    <Bar />
  </div>
</div>

If we used mount() the output would be like this:

mount_example.html

<!-- mount() -->
<Foo>
  <div>
    <div className="fooClass">
      <Bar>
        <div>
          <div className="barClass">
            Something here
          </div>
        </div>
      </Bar>
    </div>
  </div>
</Foo>

As mentioned, mount() fully renders the component, including the children, where shallow() only renders the Foo component. Using the render() function, you do not have access to the .debug() method, so you can not see this output. Let’s move on, for now, and see if this .debug() comes in handy later.

We often pass props to components, sometimes we end up passing props down a chain of components. Let’s look at an example:

Foo.js

import React from 'react';

export function Foo() {
    return <div>
        <div className="fooClass">
            <Bar className="someCustomClass" />
        </div>
    </div>;
}

export function Bar(props) {
    return <div>
        <div className="barClass">
            <Baz {...props} />
        </div>
    </div>;
}

export function Baz(props) {
    return <div>
        <div {...props}>
            <h1>Baz is rendered</h1>
        </div>
    </div>;
}

In the example, Foo instantiates Bar and passes a className="someCustomClass" property to it, Bar accepts props and passes them down to Baz. This may not be “ideal,” and you can argue that, but this happens in libraries so I wanted to make a point with a very narrow example. Let’s say you want to test that your Bar element gets the someCustomClass class applied to it. Your test might end up like this:

foo.spec.js

import React from 'react';import { render, mount, shallow } from 'enzyme';import { Foo } from 'Foo';describe.only('Test for Foo component', function () {    it('can mount Foo component', function () {        const wrap = mount(            <Foo />        );        expect(wrap.find('.someCustomClass')).to.have.length(1);    });});

We are mount()ing the Foo component, and then looking to see if we can .find('.someCustomClass') and we expect there to be one, since we are passing the className to Bar. We run our tests, and get back an error that states AssertionError: expected { length: 3 } to have a length of 1 but got 3. According to our tests, we are getting back 3 elements with className="someCustomClass" instead of the one we expect. At this point, I think we can pull that .debug() function out and see if that helps us see what is going on:

Foo.spec.js

import React from 'react';import { render, mount, shallow } from 'enzyme';import { Foo } from 'Foo';describe.only('Test for Foo component', function () {    it('can mount Foo component', function () {        const wrap = mount(            <Foo />        );        console.log(wrap.find('.someCustomClass').debug());        expect(wrap.find('.someCustomClass')).to.have.length(1);    });});

Which when our tests are run, we should see something like this in the output:full_output.html

<Bar className="someCustomClass">  <div>    <div className="barClass">      <Baz className="someCustomClass">        <div>          <div className="someCustomClass">            <h1>              Baz is rendered            </h1>          </div>        </div>      </Baz>    </div>  </div></Bar><Baz className="someCustomClass">  <div>    <div className="someCustomClass">      <h1>        Baz is rendered      </h1>    </div>  </div></Baz><div className="someCustomClass">  <h1>    Baz is rendered  </h1></div>

It appears as though .debug() gives us the rendered output of each component, which is why we have repeated markup, so really we can focus on the first block:

focused_html.html

<Bar className="someCustomClass">  <div>    <div className="barClass">      <Baz className="someCustomClass">        <div>          <div className="someCustomClass">            <h1>              Baz is rendered            </h1>          </div>        </div>      </Baz>    </div>  </div></Bar>

Using .debug() we can see that there are in fact three elements that have className="someCustomClass" which explains why our test is returning three instead of the one we expect. Which brings me to the next tip. Enzyme provides a function called .hostNodes() which essentially gives us the rendered DOM elements and not the React elements. To use this in our test, we can do this:

Foo.spec.js

import React from 'react';import { render, mount, shallow } from 'enzyme';import { Foo } from 'Foo';describe.only('Test for Foo component', function () {    it('can mount Foo component', function () {        const wrap = mount(            <Foo />        );        console.log(wrap.find('.someCustomClass').hostNodes().debug());        expect(wrap.find('.someCustomClass').hostNodes()).to.have.length(1);    });});

The console.log() in the last snippet will output this:

host_nodes.html

<div className="someCustomClass">  <h1>    Baz is rendered  </h1></div>

Which gives us the single .someCustomClass element we would expect. That tip seemed to take a while to explain, let’s do a really quick one.

This happens from time to time, you run your tests and you get a bunch of what looks like gibberish or maybe it looks like unicode output. Looks something like this:

js_blob_output.js

Chrome 71.0.3578 (Mac OS X 10.14.0) ERROR  {    "message": "Uncaught Error: Module build failed (from ./node_modules/babel-loader/lib/index.js):\nSyntaxError: Unexpected token (9:10)\n\n\u001b[0m \u001b[90m  7 | \u001b[39m        \u001b[36mconst\u001b[39m wrap \u001b[33m=\u001b[39m mount(\n \u001b[90m  8 | \u001b[39m            \u001b[33m<\u001b[39m\u001b[33mFoo\u001b[39m \u001b[33m/\u001b[39m\u001b[33m>\u001b[39m\n\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m  9 | \u001b[39m        )\u001b[33m.\u001b[39m\u001b[33m;\u001b[39m\n \u001b[90m    | \u001b[39m          \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\n \u001b[90m 10 | \u001b[39m        \u001b[90m// console.log(wrap.find('.someCustomClass').hostNodes().debug());\u001b[39m\n \u001b[90m 11 | \u001b[39m\n \u001b[90m 12 | \u001b[39m        expect(wrap\u001b[33m.\u001b[39mfind(\u001b[32m'.someCustomClass'\u001b[39m)\u001b[33m.\u001b[39mhostNodes())\u001b[33m.\u001b[39mto\u001b[33m.\u001b[39mhave\u001b[33m.\u001b[39mlength(\u001b[35m1\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\nat test/js/main.js:162346:1\n\nError: Module build failed (from ./node_modules/babel-loader/lib/index.js):\nSyntaxError: Unexpected token (9:10)\n\n\u001b[0m \u001b[90m  7 | \u001b[39m        \u001b[36mconst\u001b[39m wrap \u001b[33m=\u001b[39m mount(\n \u001b[90m  8 | \u001b[39m            \u001b[33m<\u001b[39m\u001b[33mFoo\u001b[39m \u001b[33m/\u001b[39m\u001b[33m>\u001b[39m\n\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m  9 | \u001b[39m        )\u001b[33m.\u001b[39m\u001b[33m;\u001b[39m\n \u001b[90m    | \u001b[39m          \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\n \u001b[90m 10 | \u001b[39m        \u001b[90m// console.log(wrap.find('.someCustomClass').hostNodes().debug());\u001b[39m\n \u001b[90m 11 | \u001b[39m\n \u001b[90m 12 | \u001b[39m        expect(wrap\u001b[33m.\u001b[39mfind(\u001b[32m'.someCustomClass'\u001b[39m)\u001b[33m.\u001b[39mhostNodes())\u001b[33m.\u001b[39mto\u001b[33m.\u001b[39mhave\u001b[33m.\u001b[39mlength(\u001b[35m1\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\n    at Object../test/js/components/actions/Foo.spec.js (test/js/main.js:162346:7)\n    at __webpack_require__ (webpack:///webpack/bootstrap:19 <- test/js/main.js:20:30)\n    at webpackContext (webpack:///test/js%20sync%20/.spec/.js$:99 <- test/js/main.js:160615:9)\n    at Array.forEach (<anonymous>)\n    at Object.<anonymous> (webpack:///test/js/main.js:26:19 <- test/js/main.js:160678:20)\n    at Object../test/js/main.js (test/js/main.js:160679:30)\n    at __webpack_require__ (webpack:///webpack/bootstrap:19 <- test/js/main.js:20:30)\n    at webpack:///webpack/bootstrap:83 <- test/js/main.js:84:18\n    at test/js/main.js:87:10",    "str": "Uncaught Error: Module build failed (from ./node_modules/babel-loader/lib/index.js):\nSyntaxError: Unexpected token (9:10)\n\n\u001b[0m \u001b[90m  7 | \u001b[39m        \u001b[36mconst\u001b[39m wrap \u001b[33m=\u001b[39m mount(\n \u001b[90m  8 | \u001b[39m            \u001b[33m<\u001b[39m\u001b[33mFoo\u001b[39m \u001b[33m/\u001b[39m\u001b[33m>\u001b[39m\n\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m  9 | \u001b[39m        )\u001b[33m.\u001b[39m\u001b[33m;\u001b[39m\n \u001b[90m    | \u001b[39m          \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\n \u001b[90m 10 | \u001b[39m        \u001b[90m// console.log(wrap.find('.someCustomClass').hostNodes().debug());\u001b[39m\n \u001b[90m 11 | \u001b[39m\n \u001b[90m 12 | \u001b[39m        expect(wrap\u001b[33m.\u001b[39mfind(\u001b[32m'.someCustomClass'\u001b[39m)\u001b[33m.\u001b[39mhostNodes())\u001b[33m.\u001b[39mto\u001b[33m.\u001b[39mhave\u001b[33m.\u001b[39mlength(\u001b[35m1\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\nat test/js/main.js:162346:1\n\nError: Module build failed (from ./node_modules/babel-loader/lib/index.js):\nSyntaxError: Unexpected token (9:10)\n\n\u001b[0m \u001b[90m  7 | \u001b[39m        \u001b[36mconst\u001b[39m wrap \u001b[33m=\u001b[39m mount(\n \u001b[90m  8 | \u001b[39m            \u001b[33m<\u001b[39m\u001b[33mFoo\u001b[39m \u001b[33m/\u001b[39m\u001b[33m>\u001b[39m\n\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m  9 | \u001b[39m        )\u001b[33m.\u001b[39m\u001b[33m;\u001b[39m\n \u001b[90m    | \u001b[39m          \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\n \u001b[90m 10 | \u001b[39m        \u001b[90m// console.log(wrap.find('.someCustomClass').hostNodes().debug());\u001b[39m\n \u001b[90m 11 | \u001b[39m\n \u001b[90m 12 | \u001b[39m        expect(wrap\u001b[33m.\u001b[39mfind(\u001b[32m'.someCustomClass'\u001b[39m)\u001b[33m.\u001b[39mhostNodes())\u001b[33m.\u001b[39mto\u001b[33m.\u001b[39mhave\u001b[33m.\u001b[39mlength(\u001b[35m1\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\n\n    at Object../test/js/components/actions/Foo.spec.js (test/js/main.js:162346:7)\n    at __webpack_require__ (webpack:///webpack/bootstrap:19 <- test/js/main.js:20:30)\n    at webpackContext (webpack:///test/js%20sync%20/.spec/.js$:99 <- test/js/main.js:160615:9)\n    at Array.forEach (<anonymous>)\n    at Object.<anonymous> (webpack:///test/js/main.js:26:19 <- test/js/main.js:160678:20)\n    at Object../test/js/main.js (test/js/main.js:160679:30)\n    at __webpack_require__ (webpack:///webpack/bootstrap:19 <- test/js/main.js:20:30)\n    at webpack:///webpack/bootstrap:83 <- test/js/main.js:84:18\n    at test/js/main.js:87:10"  }

All you really need to know is that this is most likely a syntax error in your test file. If you scroll up, through enough gibberish, you should see something pointing out the syntax error:

syntax_error_output

   7 |         const wrap = mount(   8 |             <Foo />>  9 |         ).;     |           ^
Here I added a . in order to cause the test runner to have a meltdown. So instead of reading the wall of cryptic unicode text, just scroll up and you will probably find something way more helpful. Next tip!!

So let’s look at a more real-world test:

EditDatasource.spec.js

import React from 'react';import { render, mount, shallow } from 'enzyme';import EditDatasource from './EditDatasource';describe('tests for EditDatasource', function () {  it.only('changes the datasource.type when a select button is clicked', function () {    const wrap = mount(        <EditDatasource            notify={notify}            navigate={navigate}            action={actionSetup()} />    );    expect(wrap.state().datasource.type === 'datawarehouse').to.be.true;    wrap.find('.manualUpload').simulate('click');    expect(wrap.state().datasource.type === 'manual').to.be.true;  });});

We can see our EditDatasource component is mounted, a pre-assertion is done to make sure the state.datasource.type === 'datawarehouse', we simulate a click, and then assert the state.datasource.type has changed to manual. This is pretty straightforward, but let’s say you want to use Link from react-router-dom package, and you have already built your component that works in the browser since your EditDatasource component is rendered by a Route component from react-router-dom. In your browser it’s going to work fine, because Link has access to, or lives within a Route component. So you add Link, test it in the browser, everything works. Feeling good, go to run your tests just to make sure everything else works…and you get an error, like this: Error: Uncaught Invariant Violation: You should not use outside a It turns out that mounting EditDatasource directly means that the Route component that renders it does not exist, and this is causing a problem for the Link component. Luckily the people behind react-router-dom make this pretty easy to work around, and provide a MemoryRouter component. So we are going to add this to our test, and will fix our problem. The test now looks like this:

EditDatasource.spec.js

import React from 'react';import { render, mount, shallow } from 'enzyme';import EditDatasource from './EditDatasource';import { MemoryRouter } from 'react-router-dom';describe('tests for EditDatasource', function () {  it.only('changes the datasource.type when a select button is clicked', function () {    const wrap = mount(      <MemoryRouter>        <EditDatasource            notify={notify}            navigate={navigate}            action={actionSetup()} />      </MemoryRouter>    );    expect(wrap.state().datasource.type === 'datawarehouse').to.be.true;    wrap.find('.manualUpload').simulate('click');    expect(wrap.state().datasource.type === 'manual').to.be.true;  });});

We run our tests, and as we expect, we get a different error…wait…what? The error now says “TypeError: Cannot read property ‘datasource’ of null”. Thinking about this, all we did is wrap EditDatasource in a MemoryRouter so that Link would play nice, we didn’t change anything else. And this is why this section is titled “Understanding the Wrapper.” When we wrapped EditDatasource in MemoryRouter, our test wrap now contains an instance of ReactWrapper around MemoryRouter. So when we do wrap.state() we are looking in the state of MemoryRouter which does not care about our datasource.type. Now you might think, “Okay cool, just need to .find() the EditDatasource component and get that state,” so something like this: wrap.find(EditDatasource).state().datasource.type However, you will get an error Error: ReactWrapper::state() can only be called on the root. Which is where the instance() method comes in. What we need to do is this: wrap.find(EditDatasource).instance().state.datasource.type

Note that state is now a property instead of a function.

And our final tests look like this:EditDatasource.spec.js

import React from 'react';import { render, mount, shallow } from 'enzyme';import EditDatasource from './EditDatasource';import { MemoryRouter } from 'react-router-dom';describe('tests for EditDatasource', function () {  it.only('changes the datasource.type when a select button is clicked', function () {      const wrap = mount(          <MemoryRouter>              <EditDatasource                  notify={notify}                  navigate={navigate}                  action={actionSetup()} />          </MemoryRouter>      );      expect(wrap.find(EditDatasource).instance().state.datasource.type === 'datawarehouse').to.be.true;      wrap.find('.manualUpload').simulate('click');      expect(wrap.find(EditDatasource).instance().state.datasource.type === 'manual').to.be.true;  });});

The key here is to understand that the Wrapper wraps around the most root component, and in some cases, you will need to dig in, in order to get the state or methods you are after.

Insights meet inbox

Sign up for weekly articles & resources.

Good luck!


Posted by Andy Huggins

Related Content

thumbnail image

Get Inspired

3 Tips for Showing Value in the Tech You Build

5 MINUTES READ | April 24, 2019

Get Informed

A Beginner’s Experience with Terraform

4 MINUTES READ | December 20, 2018

Get Informed

Tips for Holiday Reporting Preparedness

3 MINUTES READ | November 5, 2018

thumbnail image

Get Insights

Navigating the Amazon Ecosystem

2 MINUTES READ | September 10, 2018

thumbnail image

Get Insights

Our Approach to Marketing Automation

7 MINUTES READ | November 16, 2017

ALL POSTS