A STEP-BY-STEP TDD APPROACH ON TESTING REACT COMPONENTS USING ENZYME

That's a mouthful title, but it's the best I could do. Coffee's not in my system yet and the guy at the coffeeshop knows no other method than waiting for the effect. And in programming, waiting is death :). So on we go...

A NOTE ON TDD

I'd like to open with some ideas on what I think TDD is and what isn't. I've seen and heard a lot of people preaching the TDD approach to one's codebase and for good reasons. TDD is a very powerful, time saving technique (I'm tempted to call this aframework as well).

But most of the times, I hear TDD being linked to the idea of tests, specifically unit tests. Now, that may not be a surprise to many. After all, TDD stands for Test Driven Development, so a "distant" relation with the notions of testing is to be expected.

But when I stumbled upon this article I felt I finally found someone that shared the same ideas as I had for a while. Which is a liberating feeling by the way. I don't see (I actually believe it's restrictive and harmful to share this view) TDD as being something specifically related to testing. I see it as a framework that imposes a certain problem thinking model based on the RGR (Red/Green/Refactor) cycle and that uses tests as a mean of validating the entire workflow.

I think the paradigm behind TDD is much more important and much more valuable to one's professional development than the ties that we promote with tests. Getting this mental model engrained in your thinking will have huge benefits when tackling any software (dare I say not only?) problem, whether it will be architectural, implementation, performance, etc.

Last but not least, I feel TDD as being closest to the way we develop software right now. Gone are the days of monolithic programs, waterfall approaches and lengthy processes. We embrace fast releases, short cycles and plenty of user/client feedback. Facebook's motto "Move fast and break things" seems pretty accurate and paints a descriptive picture of how we build software nowadays. In this fast paced world, TDD is the only paradigm that provides a bit of sanity. I feel it's built to allow experimenting and embrace change, which are the only constants we can be sure of in our development.

Now let's dissect the notions a bit.

WHAT IS TDD?

Kent Beck wrote an excellent book titled Test Driven Development By Example in which he talks about the Red/Green/Refactor cycle in detail. RGR is the backbone idea in TDD. It emphasises the way of thinking about and approaching a problem.

The whole flow promotes the idea that the only time you write new code is only after you have a failing automated test. Then you focus on improving the code you wrote. Making it more readable, more concise, easier to understand and as simple as possible. You eliminate all duplication and the tests you already have are there as a safety net.

So you start from a red phase (failing test) after which you write code and make the test pass (green). The important thing to understand is that you shouldn't focus on writing perfect, snowflake-like code. Just make it work. Even if it's stupid. Then you improve (refactor) by shaping the already written code into something that still meets the expectations and does what it has to do but it's dead simple.

Thus, refactoring is iterative. You may undergo multiple cycles of refactoring for a piece of code/functionality. After you're happy with the results, you focus on adding extra functionality to that code by repeating the cycle all over again. Write another test (maybe for an edge case you haven't thought of earlier), watch it fail, make it work, refactor. Rinse and repeat.

I find this to be a very powerful technique. Not only it forces me to constantly improve my code and think of better and simpler ways to communicate my intentions or implement the feature, but the fact that I have to write the test first means I often need to think about the functionality and the API/interface as soon as possible. This proved me times after times that it leads to a better design of your component/module/API/etc. and a smaller overall footprint for it.

TDD can be easily applied in the React ecosystem as well. I stumbled upon Enzyme some time ago and I fell in love with it immediately. If you haven't had the change to work with it, I highly recommend you check it out. It's very well documented and it has grown to be my preferred testing framework for React code. This tutorial will not focus on detailing Enzyme's API (you can find that online), but rather point out some high-level concepts (shallow rendering, DOM rendering and the TDD approach). So let's drop the theory and let's see how we can put this in practice.

TDD IN REACT USING ENZYME

Ok, so let's say that we need to create a UsersList component. Something that will display the name and the age of each person in some list. Now we'll break this into two components.

The first we will call the User component. It will be implemented a stateless functional component whose only purpose will be to render some data passed in via props. The second one we will callUsersList. This component will hold the logic for fetching data from a remote server and rendering the list of users by using theUser component. If it doesn't make much sense right now don't worry. As we go along it will unwind.

But before we start, let's first try to understand how Enzyme can help us and dissect the notions of shallow vs full DOM rendering.

ENZYME, SHALLOW RENDERING, FULL DOM RENDERING

Enzyme is AirBnB's product. By their words:

Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output.

Enzyme's API is meant to be intuitive and flexible by mimicking jQuery's API for DOM manipulation and traversal.

What I find so awesome about it it exactly this. Their API. How dead simple and easy to use it is. It's very well documented. And it also provides nice abstractions for shallow rendering and full DOM rendering, which was a game changer for me.

First, let's ask ourselves: "What is shallow rendering?". Well it's a way to test your React components by rendering them only one level deep, which allows you to focus on the render method, without worrying about child components, etc. It's very helpful for stateless functional components and for quick markup assertion.

Shallow rendering does not require a DOM. This means we will not have access to component lifecycle methods (eg:componentWillMountcomponentDidMount, etc.), refs, etc. But for our User component, this testing approach will work just fine.

Full DOM rendering on the other side, implies the existence of some sort of DOM-like structure. This is achieved, in memory, by the use of jsdom package. Full DOM rendering means we will render the entire structure of the component (including children) and that we will have access to component lifecycle methods.

This is a more thorough approach to testing a component, but it's needed in some cases. For example, our UsersList component will request data from a remote server in componentDidMount. We would like to test that the call actually happens, that setState is called, etc.

Because of the amount of work it does, full DOM rendering is slower than shallow rendering. But if you want to test logic from component lifecycle methods, have access to refs, assert deep children components' markups, etc., then it's the right way to go.

There is also a third approach called static rendering. In short, it's a mix between the two, resulting in the rendered markup from therender method. I don't find the need to use it that often, but if you're interested, you can find some details here.

Now, a short word about the project's structure.

SETTING UP THE PROJECT

I already made this available on Github. Clone the repo from hereand follow the instructions in the README.md file.

The setup brings in chai as the assertion library and sinon for creating spies. I love these two and I think they complement well with Enzyme's abilities. Feel free to try your favourite libs if you don't fancy these though.

A note on the initial setup. You might notice a file called setup.jssomewhere in the project. It's also being referenced when we defined the test command in package.json:


"scripts": {
  "test": "mocha setup.js tests.js"
}

This file is a helper that provides some initial configuration we will need when rendering components to the DOM. In order to be able to do that, we need a virtual representation of the DOM (we don't have a real one like we do in the browser) which is achieved by using the very popular jsdom package. We need to also expose some globally accessible object like windowdocument ornavigator. So that's what this file takes care of.

Ok, back to writing code :).

TESTING THE USER COMPONENT

Remember the two principles behind the TDD approach? If yes, what's the first thing we need to to? We'll write a failing automated test, of course!

A simple implementation might be (btw this is too simple but I'm trying to exemplify the RGR cycle, so bear with me):


import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';


describe('Test suite for User component', () => {
  it('UserComponent should exist', () => {
    let wrapper = shallow(<User />)
    expect(wrapper).to.exist;
  });
});

This one will initially fail. Of course it will, we don't even have theUser component defined! We're in the Red stage. So let's move to Green, by making the test pass. Create the userComponent file and adjust the import path if needed. A simple implementation might be (put this before your test):


const UserComponent = (props) => {
  return null;
};

Awesome! Now the test passes. Of course, this is very simple and not helpful. But it's a start. We must now think of the Refactoring phase. Important thing to remember: Refactor only when you need to. Do you have duplication? Can you simplify your code? If the answer is yes, do it. If not, move forward.

Looking at what we wrote so far, I'd say there isn't much to refactor. So let's continue our endeavour. Our component, in its current state, is doing nothing. Which is not what we want. Remember that we want to display a user's name and age, that the component will receive via props. So let's think of a simple test that will assert for the existence of a markup to present these information.

The code looks like this:


it('Correctly displays the user name and age in paragraphs wrapped under a parent div', () => {
  let wrapper = shallow(<UserComponent
                            name={ 'Reign' }
                            age={ 26 } />);

  expect(wrapper.type()).to.equal('div');
  expect(wrapper.hasClass('user')).to.equal(true);

  let namePar = wrapper.childAt(0);
  expect(namePar.type()).to.equal('p');
  expect(namePar.hasClass('user__name')).to.equal(true);
  expect(namePar.text()).to.equal('Name: Reign');

  let agePar = wrapper.childAt(1);
  expect(agePar.type()).to.equal('p');
  expect(agePar.hasClass('user__age')).to.equal(true);
  expect(agePar.text()).to.equal('Age: 26');
});

Now a lot of things happen here. See how we were forced to think about the implementation details up front? What the markup will be, what classes should we have on our component? I think this forcing bring only good to the table. It's a matter of thinking before doing which should always happen in software development (spoiler alert: unfortunately it doesn't!).

The test does not pass. We're back to the Red phase. So let's modify our User component so that we can go back to the Green stage.

An example:


const UserComponent = (props) => {
  return (
    <div className="user">
      <p className="user__name">Name: { props.name }</p>
      <p className="user__age">Age: { props.age }</p>
    </div>
  );
};

Awesome! The test is passing now. We've successfully coded our components, made use of props to display data and kept a simple markup. Now a question: is there anything to refactor? Well, I would say, by looking at the User component code, no.

But here's a catch. Your components shouldn't be the only thingyou should look at. Your test code is a living thing as well! If you take a closer look there, you'll see we are repeating ourselves by calling let wrapper = shallow() in each test. We can definitely do better. Mocha has a helper function that allows you to pass in code to be executed before each test specifically so you can follow the DRY (Don't Repeat Yourself) rule. So let's refactor our tests:


describe('Test suite for UserComponent', () => {
  beforeEach(() => {
    // Prevent duplication
    wrapper = shallow(<UserComponent
                            name={ 'Reign' }
                            age={ 26 } />);
  });

  it('UserComponent should exist', () => {
    expect(wrapper).to.exist;
  });

  it('Correctly displays the user name and age in paragraphs wrapped under a parent div', () => {
    expect(wrapper.type()).to.equal('div');
    // more code...
  });
});

Yay! Better code already. Now we're done with the Usercomponent. Let's move to the wrapper. The UsersList component.

TESTING THE USERSLIST COMPONENT

Let's remember the goal. UsersList will basically act as acontainer for User (a very good article about smart/dumb or container/presentational components was written by Dan Abramov and you can find it here). Its goal is to fetch data from a remote server (we will rely on superagent and GitHub's API for this) and construct the list of users.

So we know we will have to use a full DOM rendering approach to test this component because the AJAX call will be done incomponentDidMount (this is a best practice) and we will want to assert the children's markup as well (see that the actual list of users is being constructed).

The flow looks like this:

  1. Mount component with an empty state
  2. Render will return null because no users yet
  3. componentDidMount will fire and it will initiate AJAX call
  4. We get back some data, call setState with an array of users
  5. The component re-renders, this time with some user data. A new markup can be asserted (a wrapper div containing someUser components)

Using TDD approach, let's first write a failing test to illustrate the above scenario (in order). An example might be:


describe('Test suite for UsersListComponent', () => {
  beforeEach(() => {
    // Note the `beforeEach` from the start
    wrapper = mount(<UsersListComponent />);
  });

  it('Renders null based on the initial state (empty `usersList` array)', () => {
    expect(wrapper.state().usersList).to.be.instanceof(Array);
    expect(wrapper.state().usersList.length).to.equal(0);
    expect(wrapper.html()).to.equal(null);
  });
});

This fails as we expected. This time we anticipated the multiple usage for wrapper = mount(); so we created this in beforeEach right from the start. Let's go to the Green stage by quickly mocking something that can make our test pass.


class UsersListComponent extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      usersList: []
    };
  }

  render() {
    return null;
  }
}

Again, not that much to refactor here. Now let's focus on the next thing. Considering we would already have a users array (valid data source), we would expect a different markup. So let's not worry about how we will get hold of the data, but focus on what we will do after we have it. We know we want to render a parent div that will hold all the children (User components) inside. Let's write a test for something like this. Note we will manually trigger a state change right after the component mounted. Later, this will be done in the success function of the AJAX call.


it('Renders the root `div` with the right class and renders correct children of type `User`', () =&gt; {
  wrapper.setState({
    usersList: [
      {
        name: 'Reign',
        age: 26
      }
    ]
  });
  expect(wrapper.state().usersList).to.be.instanceof(Array);
  expect(wrapper.state().usersList.length).to.equal(1);
  expect(wrapper.find('.users-list')).to.have.length(1);

  let child = wrapper.childAt(0);
  let childProps = child.props();

  expect(child.type()).to.equal(UserComponent);
  expect(childProps.name).to.equal('Reign');
  expect(childProps.age).to.equal(26);
});

Now let's refactor our render method, since this test fails. We need to add some conditional logic there. An idea would be:


render() {
  if (!this.state.usersList.length) {
    return null;
  }

  return (
    <div className="users-list">
      {
        this.state.usersList.map((user, index) => {
          return (
            <UserComponent
                  key={ index }
                  name={ user.name }
                  age={ user.age } />
          );
        })
      }
    </div>
  );
}

Note how we assert on the actual creation of the User component, not necessarily its markup (you can use static rendering for that). I find it unnecessary to duplicate markup checks since this is logic that belongs in the User component anyway and it should be thoroughly tested there. Now can we improve our code further? I think the answer in this case is yes.

One thing I don't like is lots of code in render. I always prefer extracting bits and pieces of markup creation into their own methods. This not only improves the readability of the overall function, but it aids a lot when you have if/else inside the code and you test a specific branch. You can only see that the call to a particular user defined method was made and do the actual asserts when testing that specific piece of code.

So let's extract our rendering of the users list into a separate function that we'll call _constructUsersList. The tests need to change. We need to check now that render calls this new function, and we move the overall logic of testing the effect of it into a new test.


it('Renders the root `div` with the right class and calls `_constructUsersList` to create the users list', () => {
  sinon.spy(UsersListComponent.prototype, '_constructUsersList');
  wrapper.setState({
    usersList: [
      {
        name: 'Reign',
        age: 26
      }
    ]
  });
  expect(wrapper.find('.users-list')).to.have.length(1);
  expect(UsersListComponent.prototype._constructUsersList.calledOnce).to.equal(true);
});

it('The `_constructUsersList` behaves correctly', () => {
  wrapper.setState({
    usersList: [
      {
        name: 'Reign',
        age: 26
      },
      {
        name: 'Vlad',
        age: 30
      }
    ]
  });
  const res = wrapper.instance()._constructUsersList();
  expect(res).to.be.instanceof(Array);
  expect(res.length).to.equal(2);
  expect(mount(res[0]).type()).to.equal(UserComponent);
  expect(res[0].props.name).to.equal('Reign');
  expect(res[0].props.age).to.equal(26);
  expect(mount(res[1]).type()).to.equal(UserComponent);
  expect(res[1].props.name).to.equal('Vlad');
  expect(res[1].props.age).to.equal(30);
});

Note the use of sinon and sinon.spy(). This is how we check for a particular function call. For more info on their API, check the docs here. Also, we can now call the _constructUsersList method in an imperative way by using wrapper.instance() provided by the Enzyme framework. I find this quite convenient :). Let's now adjust our component code. We need to change the rendermethod as well as add the new _constructUsersList function.


render() {
  if (!this.state.usersList.length) {
    return null;
  }

  return (
    <div className="users-list">
      { this._constructUsersList() }
    </div>
  );
}

_constructUsersList() {
  return this.state.usersList.map((user, index) => {
    return (
      <UserComponent
            key={ index }
            name={ user.name }
            age={ user.age } />
    );
  });
}

All right. We're in a better position now. Only one thing left to do. Write an AJAX call in componentDidMount that will fetch data from a remote server and update state on success. Let's fail with some test for that! (we'll write two - one for


it('Correctly updates the state after AJAX call in `componentDidMount` was made', (done) => {
  nock('https://api.github.com')
    .get('/users')
    .reply(200, [
      { 'name': 'Reign', 'age': 26 }
    ]);
  // Overwrite, so we can correctly reason about the count number
  // Don't want shared state
  wrapper = mount(<UsersListComponent />);
  setTimeout(function() {
    expect(wrapper.state().usersList).to.be.instanceof(Array);
    expect(wrapper.state().usersList.length).to.equal(1);
    expect(wrapper.state().usersList[0].name).to.equal('Reign');
    expect(wrapper.state().usersList[0].age).to.equal(26);
    nock.cleanAll();
    done();
  }, 1500);
});

A couple of things happen here worth explaining. First, in order to mock a server response, I'm using the nock package (more detailshere). I know sinon has its own createFakeServer() method, but I've had issues with it. If anyone can point me to a working jsbin/example using that, I'd really appreciate it.

Second, we're mounting the component once more. I'm doing that because I don't want any old state to conflict with the new one that I'm gonna set, and I also want the componentDidMount lifecycle method to be trigger at the exact same time that particular test is running (this is a case where mounting in beforeEach bites you in the ass).

Third, we have a timeout there. I admit 1500 ms is an arbitrary value (Mocha stops at 2000 by default, but it's configurable). The reason we need this is because we need to allow the component some time, after mounting, to execute the AJAX call and set the new state (for more info on how setState really works under the hood, check out my other article). For me, setting a timeout less than 20 ms fails. Again, if anyone is aware of a different approach, I'd appreciate a helpful comment :).

Now let's make the final changes to our component. An example for the componentDidMount method might be:


componentDidMount() {
  request
    .get('https://api.github.com/users')
    .end((err, res) => {
      if (err) {
        console.log(err);
        return;
      }

      this.setState({
        usersList: res.body.slice(0)
      });
    });
}

A prerequisite is to import superagent up top. Something likeimport request from 'superagent'. Hurray, all our tests are green again!

One thing we haven't touched is the error case. Which should always be treated. But I guess this can remain as a homework for you. Just remember to follow the same TDD mindset - think about the implementation, write a failing automated test (Red), make it pass with crappy/not perfect code (Green), improve iteratively on what you wrote (Refactor).

CLOSING REMARKS

Quite a lengthy article for just two simple React components. But I hope I've managed to illustrate a bit the whole philosophy behind Test Driven Development. And that it's more of a framework, a mindset that you can apply to any problem, rather than something specifically ties to tests.

We've tackled unit tests exclusively here. But the same paradigm can be applied when writing functional or end-to-end tests. Enzyme has a basic simulate() functionality in place for writing functional tests. You can look at the docs, but make sure you understand the gotchas.

I feel TDD is something that can dramatically improve the quality of one's code, its reliability, its cleanliness and its predictability. Applying TDD as a coding technique to the future pieces of software you'll write will be a challenge and shift in your mindset. But I guarantee you that, once you'll get the hang of it, you'll fall in love with it and save tons of time for your products.

Looking forward to your comments. Stay safe, smile often and practice TDD!

PS: The cactus emoji is RIGHT HERE -> ��.

POSTED BY REIGN ON 23 JULY 2016

SHARE ON TWITTER

SHARE ON FACEBOOK

WRITTEN WITH LOVE

你可能感兴趣的:(React)