Tested React: Let’s build a Data Table

Gasim Gasimzada
Frontend Weekly
Published in
7 min readDec 4, 2018

“Tested React” is a series of guides to get people accustomed to testing components in React ecosystem. This series IS NOT about setting up testing environments for React — we will be using Create React App, Jest (comes with CRA), and Enzyme (needs to be installed and configured) for a nice interface to write tests to build and test our components. In this series, the goal is to help you build intuition on what to test in your React apps.

Before we get into the guide, I want to say something. It is okay if your tests are bad. It is okay if your tests fail and need to updated for refactoring. If you are uncertain about the quality or scope of your tests (e.g when you start a new project), it is okay. Eventually, you will make your tests better. So, do not be scared of testing because you will get it wrong. Just like anything in development, with more experience and clearer scope, you will end up writing better tests.

Now let’s get to business.

Install Enzyme

To install enzyme, we need to perform two actions. First, let’s install enzyme and its React adapter:

$ yarn add --dev enzymeenzyme-adapter-react-16# If you use NPM, use the following$ npm install --save-dev enzyme enzyme-adapter-react-16

Then, we need to add enzyme adapter to work with React. Fortunately, if you create a file named src/setupTests.js in CRA, the setup will happen before running tests:

import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

That’s it! Now enzyme is setup and we can use it in our tests.

Data Table — A simple Spec

Before we write any software, it is a good idea to think and even write it down on a piece of paper on what our component will cover.

So, what will our component do? A data table component accepts two properties: data in form of array and columns in form of array. The component renders an HTML table with columns and loops through the data to render it. Each column object has two values in it: header and name. header will be used when rendering table headers (th tags). name will be used to map each row in the data property to a column (essentially what to render in td tags).

Now onto the test. Based on the given specification, we have to test for two important things: If there is data and if there is no data.

We know that we need a table element and a table element must have thead and tbody tags. thead tags will have table rows (tr) with th tags while tbody tags will have table rows with td tags. So, we are going to test to know that all the elements exist in the needed number (i.e if there are 3 columns, each table row must have 3 th or td tags):

// src/data-table/DataTable.test.jsimport React from 'react';import DataTable from './DataTable';it('renders in table rows based on provided columns', () => {
const cols = [
{ header: 'ID', name: 'id' },
{ header: 'Name', name: 'name' },
{ header: 'Email', name: 'email' }
];
const data = [
{ id: 5, name: 'John', email: 'john@example.com' },
{ id: 6, name: 'Liam', email: 'liam@example.com' },
{ id: 7, name: 'Maya', email: 'maya@example.com', someTest: 10 },
{ id: 8, name: 'Oliver', email: 'oliver@example.com', hello: 'hello world' },
{ id: 25, name: 'Amelia', email: 'amelia@example.com' }
];
// Shallow render Data Table
const container = shallow(<DataTable data={data} cols={cols} />);
// There should be ONLY 1 table element
const table = container.find('table');
expect(table).toHaveLength(1);
// The table should have ONLY 1 thead element
const thead = table.find('thead');
expect(thead).toHaveLength(1);
// The number of th tags should be equal to number of columns
const headers = thead.find('th');
expect(headers).toHaveLength(cols.length);
// Each th tag text should equal to column header
headers.forEach((th, idx) => {
expect(th.text()).toEqual(cols[idx].header);
});
// The table should have ONLY 1 tbody tag
const tbody = table.find('tbody');
expect(tbody).toHaveLength(1);
// tbody tag should have the same number of tr tags as data rows
const rows = tbody.find('tr');
expect(rows).toHaveLength(data.length);
// Loop through each row and check the content
rows.forEach((tr, rowIndex) => {
const cells = tr.find('td');
expect(cells).toHaveLength(cols.length);
expect(cells.at(0).text()).toEqual(data[rowIndex].id);
expect(cells.at(1).text()).toEqual(data[rowIndex].name);
expect(cells.at(2).text()).toEqual(data[rowIndex].email);
});
});

I have added comments for clarification of each row. The functions find, at, forEach, text are all part of enzyme API. Check out their API reference for more info.

We use shallow rendering ONLY renders the component itself. If you have child components, shallow rendering will not render the contents of the child components. In our case, it doesn’t really matter because DataTable component cannot have children.

Note: I want you to pay attention to the data array items with ID of 7 and 8. I have added an additional entry to each object to make sure that they are not being rendered. The way I know that they are not being rendered is by two assertions: Number of cells in a row must be equal to number of columns in data table; and content of each cell must match the content of three entries in the data.

Now that we have our tests, we can write our code until our test succeeds. Let’s create DataTable.js file in the same directory as the test file and create a component:

// src/data-table/DataTable.jsimport React from 'react';const DataTable = props => {
return (
<table>
<thead>
<tr>
{props.cols.map(col =>
<th key={col.name}>{col.header}</th>
)}
</tr>
</thead>
</table>
);
}
export default DataTable;

Now, when we run our tests, part of the test suite is rendered properly. Now, onto rendering body of the table:

const DataTable = props => {
return (
<table>
<thead>
<tr>
{props.cols.map(col =>
<th key={col.name}>{col.header}</th>
)}
</tr>
</thead>
<tbody>
{props.rows.map(row =>
<tr key={row.id}>
{props.cols.map(col =>
<td key={col.name}>{row[col.name]}</td>
)}
</tr>
)}
</tbody>
</table>
);
}

Our tests pass, the code renders, everyone is happy! Now onto the empty state.

In our empty state, we need to create a single cell that spans across the entire row and displays some text inside. We know from HTML days that, we can use colSpan to span the column across the row. So, let’s use this knowledge and write our test:

it('renders empty message as table cell if there is no data', () => {
const cols = [
{ header: 'ID', name: 'id' },
{ header: 'Name', name: 'name' },
{ header: 'Email', name: 'email' }
];
// Shallow render the data table
const container = shallow(<DataTable data={[]} cols={cols} />);
// Copy-Paste from previous test: // There should be ONLY 1 table element
const table = container.find('table');
expect(table).toHaveLength(1);
// The table should have ONLY 1 thead element
const thead = table.find('thead');
expect(thead).toHaveLength(1);
// The number of th tags should be equal to number of columns
const headers = thead.find('th');
expect(headers).toHaveLength(cols.length);
// Each th tag text should equal to column header
headers.forEach((th, idx) => {
expect(th.text()).toEqual(cols[idx].header);
});
// The table should have ONLY 1 tbody tag
const tbody = table.find('tbody');
expect(tbody).toHaveLength(1);
// END Copy Paste // There should be ONLY one table row
const row = tbody.find('tr');
expect(row).toHaveLength(1);
// The table row should have ONLY one cell
const cell = row.find('td');
expect(cell).toHaveLength(1);
// The cell should have colSpan that is equal to the number of columns
expect(cell.prop('colSpan')).toEqual(cols.length);
// Check cell text
expect(cell.text()).toEqual('There is no data in this table');
});

Now, we want to implement empty state into our data table. However, it will require a lot of {} in our JSX code, which will make it unreadable. So, let’s refactor our existing code by moving data rendering loop into its own function:

const renderData = (data, cols) =>
data.map(row =>
<tr key={row.id}>
{cols.map(col =>
<td key={col.name}>{row[col.name]}</td>
)}
</tr>
);
const DataTable = props => {
return (
<table>
<thead>
<tr>
{props.cols.map(col =>
<th key={col.name}>{col.header}</th>
)}
</tr>
</thead>
<tbody>
{renderData(props.data, props.cols)}
</tbody>
</table>
);
}

After refactoring, our first test did not fail. It is a little assurance that, our test does its job well. Now that we have refactored some code, we can add our empty state code:

const renderEmptyState = cols =>
<td colSpan={cols.length}>There is no data in this table</td>
;
const DataTable = props => {
return (
<table>
<thead>
<tr>
{props.cols.map(col =>
<th key={col.name}>{col.header}</th>
)}
</tr>
</thead>
<tbody>
{props.data.length > 0 ? renderData(props.data, props.cols) : renderEmptyState(props.cols)}
</tbody>
</table>
);
}

Tests pass, empty state is rendered properly, data state is rendered properly.

Note: I want to mention that, as you can see, I did not test anything related to styles in the component. My personal opinion on this is that, unless a style is needed for logical purposes (e.g make table striped based on a prop), I think it makes tests too specific without much added benefit.

Conclusion

The goal of this post was to test a simple component that renders a table based on props. I tried to keep it simple and only test two things but there is a lot of room for improvements and for practice, I would suggest you implement more functionality to get accustomed to testing. For example, you can add a feature that allows rendering cells in a column based on a custom function (i.e render actions column that provides links/buttons to edit or delete an item from the table).

Resources

--

--