By the end of this tutorial we are trying to make this table appear on the screen.
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.
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
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 just jump right in shall we
Lets start by creating 2 files
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.
export class UsersTableComponent {
}
I cant think of anyway to refactor this and this is ok lets just move onto the next cycle
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
import * as React from 'react';
export class UsersTableComponent extends React.Component<any, any> {
}
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 />);
});
});
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
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
It all looks amazing so far so we can skip this
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
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
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
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!
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
Looks good here, lets move onto the next cycle
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
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
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
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.
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>
);
}
}
Cant think of anything. Lets finish
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
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.