TDD with React.js and Enzyme

2017-12-17

What are we building?

By the end of this tutorial we are trying to make this table appear on the screen.

What are our goals?

  • We promise not to open a web-browser until we are finshed with our component and trust (Hope!) it will all work in the end
  • We promise to write our tests first and then our implementation
  • We promise not to get angry and throw our computers out the window

We are going to using a technique called red/green/refactor. The idea is quite simple, first we are going to write a failing test, then we are going to do the absolute minimum we can to make that test pass, finally we are going to refactor and clean up the test code and implementation. Next we are going to repeat the cycle until we are finished. Here's an image I robbed from google to help illustrate, make sure you burn it into your retinas.

What tools do we need?

We are going to be using the following tools

Id just like to take a moment to point out what Enzyme is used for. Basically it allows us to render the component we are building in memory and allows us to find and query elements within memory. There are 3 very useful ways of rendering the component

  • Shallow allows us to render a single component and none of its children. This is the main tool we will be using
  • Mount will do a full render of our component and requires a global dom. This is also usefull for testing lifecycle events such as componentDidMount
  • Render is similar to mount except it doesn't need a a global DOM

I would advise visiting the Github page and reviewing the documentation as its a hugely powerful tool. If you dont fancy setting all of the above up manually your in luck, I have prepared a boilerplate for you - https://github.com/el-davo/webpack-react-typescript-boilerplate your welcome!

Lets do this!

Lets just jump right in shall we

Cycle 1

Red

Lets start by creating 2 files

  • users-table.component.spec.tsx
  • users-table.component.tsx

lets write our first failing test

import {UsersTableComponent} from './users-table.component';

That's our first failing test. Your probably thinking that this isn't a test at all, however technically it is. For one thing we cant go any further without exporting something in users-table.component.tsx, and another thing is you are most likely seeing compile errors which means tests are failing. lets switch over to our users-table.component.tsx file and make the tests go green again.

Green

export class UsersTableComponent {
}

Refactor

I cant think of anyway to refactor this and this is ok lets just move onto the next cycle

Cycle 2

Red

import * as React from 'react';
import {shallow} from 'enzyme';
import {UsersTableComponent} from './users-table.component';

describe('<UsersTableComponent />', () => {

  beforeEach(() => {
    shallow(<UsersTableComponent />);
  });

});

This will fail again with a compilation error. Notice above shallow expects a react component and we are just giving it a class, I guess we have no choice but to make UsersTableComponent an actual react component

Green

import * as React from 'react';

export class UsersTableComponent extends React.Component<any, any> {
}

Refactor

The only thing i can think of here is we will probably want to assign the component returned from shallow method call, this will set us up nicely for the next test

import * as React from 'react';
import {shallow} from 'enzyme';
import {UsersTableComponent} from './users-table.component';

describe('<UsersTableComponent />', () => {

  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<UsersTableComponent />);
  });

});

Cycle 3

Red

import * as React from 'react';
import {shallow} from 'enzyme';
import {Card} from 'material-ui/Card';
import {UsersTableComponent} from './users-table.component';

describe('<UsersTableComponent />', () => {

  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<UsersTableComponent />);
  });

  describe('layout', () => {

    // New test
    it('should display a Card component', () => {
      wrapper.find(Card).should.have.length(1);
    });
  });
});

We probably want to test that a Card component is shown, nothing special going on here, and it should be very easy to make this pass in our component

Green

import * as React from 'react';
import {Card} from 'material-ui/Card';

export class UsersTableComponent extends React.Component<any, any> {

  constructor(props, context) {
    super(props, context);
  }

  render() {
    return (
      <Card />
    );
  }

}

So we were forced to add 2 new methods here - a constructor is needed to pass the props and context to the super class and we need to render our Card component

Refactor

It all looks amazing so far so we can skip this

Cycle 4

Red

import * as React from 'react';
import {shallow} from 'enzyme';
import {Card} from 'material-ui/Card';
import {Table} from 'material-ui/Table';
import {UsersTableComponent} from './users-table.component';

describe('<UsersTableComponent />', () => {

  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<UsersTableComponent />);
  });

  describe('layout', () => {

    it('should display a Card component', () => {
      wrapper.find(Card).should.have.length(1);
    });

    // New test
    it('should display a Table on the Card', () => {
      wrapper.find(Table).should.have.length(1);
    });
  });
});

So we are testing that a table component appears on the card

Green

import * as React from 'react';
import {Card} from 'material-ui/Card';
import {Table} from 'material-ui/Table';

export class UsersTableComponent extends React.Component<any, any> {

  constructor(props, context) {
    super(props, context);
  }

  render() {
    return (
      <Card>
        <Table />
      </Card>
    );
  }

}

And we simple render a new table

Refactor

This is where things get a little interesting, Look back at the test name "'should display a Table on the Card". Technically we don't check that its on the card, we just check that its rendered somewhere in the component, so i see one obvious way we can refactor to make our test better.

import * as React from 'react';
import {shallow} from 'enzyme';
import {Card} from 'material-ui/Card';
import {Table} from 'material-ui/Table';
import {UsersTableComponent} from './users-table.component';

describe('<UsersTableComponent />', () => {

  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<UsersTableComponent />);
  });

  describe('layout', () => {

    it('should display a Card component', () => {
      wrapper.find(Card).should.have.length(1);
    });

    it('should display a Table on the Card', () => {
      // Checking that the table is on the card
      wrapper.find(Card).find(Table).should.have.length(1);
    });
  });
});

Tests are still green after refactor, lets begin the next cycle

Cycle 5

Red

import * as React from 'react';
import {shallow} from 'enzyme';
import {Card} from 'material-ui/Card';
import {Table, TableRow} from 'material-ui/Table';
import {UsersTableComponent} from './users-table.component';

describe('<UsersTableComponent />', () => {

  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<UsersTableComponent />);
  });

  describe('layout', () => {

    it('should display a Card component', () => {
      wrapper.find(Card).should.have.length(1);
    });

    it('should display a Table on the Card', () => {
      wrapper.find(Card).find(Table).should.have.length(1);
    });

    // New test
    it('should display 2 TableRows', () => {
      wrapper.find(Card).find(Table).find(TableRow).should.have.length(2);
    });
  });
});

Checking for the presence of 2 table rows, and we learned from our previous refactor to search within the Card and the Table, Woohoo go us!

Green

import * as React from 'react';
import {Card} from 'material-ui/Card';
import {Table, TableRow} from 'material-ui/Table';

export class UsersTableComponent extends React.Component<any, any> {

  constructor(props, context) {
    super(props, context);
  }

  render() {
    return (
      <Card>
        <TableBody>
            <Table>
              <TableRow />
              <TableRow />
            </Table>
       </TableBody>
      </Card>
    );
  }

}

This is the simplest thing we can do. Wait, isn't this cheating? Not at all it satisfies all of our tests, maybe our tests aren't so great so lets see how we can lock down our tests from such deception

Refactor

Looks good here, lets move onto the next cycle

Cycle 6

Red

I'm going to create a new User interface as i think at this stage we should start testing the contents of the table. Lets create a new file in the same folder called user.interface.ts

export interface User {
  username: string;
  email: string;
}

and lets implement our failing test

import * as React from 'react';
import {shallow} from 'enzyme';
import {Card} from 'material-ui/Card';
import {Table, TableRow, TableRowColumn} from 'material-ui/Table';
import {UsersTableComponent} from './users-table.component';
import {User} from './user.interface';

describe('<UsersTableComponent />', () => {

  // Create our test data
  const USERS = [
    {username: 'user1', email: '[email protected]'},
    {username: 'user2', email: '[email protected]'}
  ] as User[];
  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<UsersTableComponent users={USERS} />);
  });

  describe('layout', () => {

    it('should display a Card component', () => {
      wrapper.find(Card).should.have.length(1);
    });

    it('should display a Table on the Card', () => {
      wrapper.find(Card).find(Table).should.have.length(1);
    });

    it('should display 2 TableRows', () => {
      wrapper.find(Card).find(Table).find(TableRow).should.have.length(2);
    });

    // New test
    it('should display user information in the table', () => {
      wrapper.find(TableRow).at(0).find(TableRowColumn).at(0).contains('user1').should.be.true();
      wrapper.find(TableRow).at(0).find(TableRowColumn).at(1).contains('[email protected]').should.be.true();

      wrapper.find(TableRow).at(1).find(TableRowColumn).at(0).contains('user2').should.be.true();
      wrapper.find(TableRow).at(1).find(TableRowColumn).at(1).contains('[email protected]').should.be.true();
    });
  });
});

Wow this is a beast of a test so lets break it down, basically we are checking does each column in each row contain the correct data. Lets see how to make this pass

Green

import * as React from 'react';
import {Card} from 'material-ui/Card';
import {Table, TableRow, TableRowColumn} from 'material-ui/Table';
import {User} from './user.interface';

interface Props {
  users: User[]
}

export class UsersTableComponent extends React.Component<Props, any> {

  constructor(props, context) {
    super(props, context);
  }

  render() {
    return (
      <Card>
        <TableBody>
        <Table>
          {
            this.props.users.map((user) => {
              return <TableRow>
                <TableRowColumn>{user.username}</TableRowColumn>
                <TableRowColumn>{user.email}</TableRowColumn>
              </TableRow>
            })
          }
        </Table>
        </TableBody>
      </Card>
    );
  }

}

And we are green again, so now our data should be displaying correctly in the table

Refactor

There is on opportunity for refactor that may not be obvious if your new to react, basically when your in a loop you need to add a key to each element returned. We can do that below

           this.props.users.map((user, key) => {
              return <TableRow key={key}>
                <TableRowColumn>{user.username}</TableRowColumn>
                <TableRowColumn>{user.email}</TableRowColumn>
              </TableRow>
            })

from experience you will learn that react will throw an error in the browser if the key is not present

Cycle 7

Red

So we prob want to write one final check to see that our component is making the correct call to fetch the list of users. If you are using Redux (We will assume we are) then this is amazingly simple. See our new test below

import * as React from 'react';
import {shallow, mount} from 'enzyme';
import {spy} from 'sinon';
import {Card} from 'material-ui/Card';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import {Table, TableRow, TableRowColumn} from 'material-ui/Table';
import {UsersTableComponent} from './users-table.component';
import {User} from './user.interface';

describe('<UsersTableComponent />', () => {

  const USERS = [
    {username: 'user1', email: '[email protected]'},
    {username: 'user2', email: '[email protected]'}
  ] as User[];

  let wrapper;
  let mounted;
  let fetchUsers;

  beforeEach(() => {
    fetchUsers = spy();

    wrapper = shallow(<UsersTableComponent users={USERS} fetchUsers={fetchUsers}/>);
    mounted = mount(<MuiThemeProvider><UsersTableComponent users={USERS} fetchUsers={fetchUsers}/></MuiThemeProvider>);
  });

  describe('layout', () => {

    it('should display a Card component', () => {
      wrapper.find(Card).should.have.length(1);
    });

    it('should display a Table on the Card', () => {
      wrapper.find(Card).find(Table).should.have.length(1);
    });

    it('should display 2 TableRows', () => {
      wrapper.find(Card).find(Table).find(TableRow).should.have.length(2);
    });

    it('should display user information in the table', () => {
      wrapper.find(TableRow).at(0).find(TableRowColumn).at(0).contains('user1').should.be.true();
      wrapper.find(TableRow).at(0).find(TableRowColumn).at(1).contains('[email protected]').should.be.true();

      wrapper.find(TableRow).at(1).find(TableRowColumn).at(0).contains('user2').should.be.true();
      wrapper.find(TableRow).at(1).find(TableRowColumn).at(1).contains('[email protected]').should.be.true();
    });
  });

  describe('actions', () => {

    // New test
    it('should call fetchUsers() on component mount', () => {
      fetchUsers.calledOnce.should.be.true();
    });
  });
});

So we are checking here if the correct call is being made, notice we are now using sinon to spy on the fetchUsers method.

Green

import * as React from 'react';
import {Card} from 'material-ui/Card';
import {Table, TableRow, TableRowColumn} from 'material-ui/Table';
import {User} from './user.interface';

interface Props {
  users: User[];
  fetchUsers();
}

export class UsersTableComponent extends React.Component<Props, any> {

  constructor(props, context) {
    super(props, context);
  }

  // Fetch the users
  componentDidMount() {
    this.props.fetchUsers();
  }

  render() {
    return (
      <Card>
       <TableBody>
        <Table>
          {
            this.props.users.map((user, key) => {
              return <TableRow key={key}>
                <TableRowColumn>{user.username}</TableRowColumn>
                <TableRowColumn>{user.email}</TableRowColumn>
              </TableRow>
            })
          }
        </Table>
        </TableBody>
      </Card>
    );
  }

}

Refactor

Cant think of anything. Lets finish

Can we run it now?

Oh your still here? I thought I had scared you off about half way through. Sure we can run it, if you do then you should see the image we were going for above

Wrap it up already!

Ok this was a long haul and i actually skipped some some believe it or not :D. This may seem like it will add so much extra time to development and i can say right now that at the start TDD will slow you down, however as you get good at it it will dramatically speed you up. One thing I always notice is it reduces the time it takes to refactor and you always have that confidence that you haven't accidentally broken anything.

My best advice for newbies to react or TDD with react is to first try and research react and write a simple component then retrofit it with some test framework of your choice, then reverse it and try writing the tests first and then implementing the component

Most importantly do not give up, If I can be a TDD superstar anyone can :D.