Getting Started with React Testing Library

I can guess what you are thinking: another React testing library? So many have already been covered here on CSS-Tricks (heck, I’ve already posted one covering Jest and Enzyme) so aren’t there already enough options to go around?

But react-testing-library is not just another testing library. It’s a testing library, yes, but one that’s built with one fundamental principle that separates it from the rest.

The more your tests resemble the way your software is used, the more confidence they can give you.

It tries to address tests for how a user will use your application. In fact, it’s done in such a way that tests won’t break even when you refactor components. And I know that’s something we’ve all run into at some point in our React journey.

We’re going to spend some time writing tests together using react-testing-library for a light to-do application I built. You can clone the repo locally:

git clone https://github.com/kinsomicrote/todoapp-test.git

And, if you do that, install the required packages next:

## yarn
yarn add --dev react-testing-library jest-dom

## npm
npm install --save-dev react-testing-library jest-dom

In case you’re wondering why Jest is in there, we’re using it for assertion. Create a folder called __test__ inside the src directory and create a new file called App.test.js.

Taking snapshots

Snapshot tests keep a record of tests that have been performed on a tested component as a way to visually see what’s changes between changes.

When we first run this test, we take the first snapshot of how the component looks. As such, the first test is bound to pass because, well, there’s no other snapshot to compare it to that would indicate something failed. It only fails when we make a new change to the component by adding a new element, class, component, or text. Adding something that was not there when the snapshot was either created or last updated.

The snapshot test will be the first test we will be writing here. Let’s open the App.test.js file and make it look like this:

import React from 'react';
import { render, cleanup } from "react-testing-library";
import "jest-dom/extend-expect";
import App from './App';

afterEach(cleanup);

it("matches snapshot", () => {
  const { asFragment } = render(<App />);
  expect(asFragment()).toMatchSnapshot();
});

This imports the necessary packages we are using to write and run the tests. render is used to display the component we want to test. We make use of cleanup to clear things out after each test runs — as you can see with the afterEach(cleanup) line.

Using asFragment, we get a DocumentFragment of the rendered component. Then we expect it to match the snapshot that had been created.

Let’s run the test to see what happens:

## yarn
yarn test

## npm
npm test

As we now know, a snapshot of the component gets created in a new folder called __snapshots__ inside the __tests__ directory if this is our first test. We actually get a file called App.test.js.snap in there that will look like this:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`matches snapshot 1`] = `
<DocumentFragment>
  <div
    class="container"
  >
    <div
      class="row"
    >
      <div
        class="col-md-6"
      >
        <h2>
          Add Todo
        </h2>
      </div>
    </div>
    <form>
      <div
        class="row"
      >
        <div
          class="col-md-6"
        >
          <input
            class="form-control"
            data-testid="todo-input"
            placeholder="Enter a task"
            type="text"
            value=""
          />
        </div>
      </div>
      <div
        class="row"
      >
        <div
          class="col-md-6"
        >
          <button
            class="btn btn-primary"
            data-testid="add-task"
            type="submit"
          >
            Add Task
          </button>
        </div>
      </div>
    </form>
    <div
      class="row todo-list"
    >
      <div
        class="col-md-6"
      >
        <h3>
          Lists
        </h3>
        <ul
          data-testid="todos-ul"
        >
          <li>
            <div>
              Buy Milk
              <button
                class="btn btn-danger"
              >
                X
              </button>
            </div>
          </li>
          <li>
            <div>
              Write tutorial
              <button
                class="btn btn-danger"
              >
                X
              </button>
            </div>
          </li>
        </ul>
      </div>
    </div>
  </div>
</DocumentFragment>
`;

Now, let’s Test DOM elements and events

Our app includes two to-do items that display by default the first time the app runs. We want to make sure that they do, in fact, show up by default on the first app run so, to test this, we have to target the unordered list (<ul>) and check the length. We expect the length to be equal to two — the number of items.

it('it displays default todo items', () => {
  const { getByTestId } = render(<App />);
  const todoList = getByTestId('todos-ul');
  expect(todoList.children.length).toBe(2);  
});

We’re making use of getByTestId in that snippet to extract the test IDs from the App component. We then set todoList to target the todos-ul element. That’s what should return as two.

Using what we’ve learned so far, see if you can write a test to assert that a user can enter values in the input field. Here are the things you’ll want to do:

  • Get the input field
  • Set a value for the input field
  • Trigger a change event
  • Assert that the input field has its value as the one you set for it in Step 2

Don’t peek at my answer below! Take as much time as you need.

Still going? Great! I’ll go grab some coffee and be right back.

Mmm, coffee. ☕️

Oh, you’re done! You rock. Let’s compare answers. Mine looks like this:

it('allows input', () => {
  const {getByTestId } = render(<App />)
  let item = 'Learn React'
  const todoInputElement = getByTestId('todo-input');
  todoInputElement.value = item;
  fireEvent.change(todoInputElement);
  expect(todoInputElement.value).toBe('Learn React')
});

Using getByTestId, I am able to extract the test IDs in the application. Then I create a variable which is set to the string Learn React, and make it the value of the input field. Next, I obtain the input field using its test ID and fire the change event after setting the value of the input field. With that done, I assert that the value of the input field is indeed Learn React.

Does that check out with your answer? Leave a comment if you have another way of going about it!

Next, let’s test that we can add a new to-do item. We’ll need to get the input field, the button for adding new items and the unordered list because those are all of the elements needed to create an new item.

We set a value for the input field and then trigger a button click to add the task. We’re able to do this by obtaining the button using getByText — by triggering a click event on the DOM element with the text Add Task, we should be able to add a new to-do item.

Let’s assert that the number of children (list items) in unordered list element is equal to three. This assumes that the default tasks are still in tact.

it('adds a new todo item', () => {
  const { getByText, getByTestId } = render(<App />);
  const todoInputElement = getByTestId('todo-input');
  const todoList = getByTestId('todos-ul');
  todoInputElement.value = 'Learn React';
  fireEvent.change(todoInputElement);
  fireEvent.click(getByText('Add Task'))
  expect(todoList.children.length).toBe(3); 
});

Pretty nice, right?

This is just one way to test in React

You can try react-testing-library in your next React application. The documentation in the repo is super thorough and — like most tools — the best place to start. Kent C. Dodds built it and has a full course on testing over at Frontend Masters (subscription required) that also covers the ins and outs of react-testing-library.

That said, this is just one testing resource for React. There are others, of course, but hopefully this is one you’re interested in trying out now that you’ve seen a bit of it but use what’s best for your project, of course.

The post Getting Started with React Testing Library appeared first on CSS-Tricks.

Getting to Grips with the Airtable API

The Airtable web app is pretty neat. You can use it like a spreadsheet but it’s useful for all sorts of other things too. The neatest thing about it for me is that it has an API so that you can treat it like a database.

I’ve been thinking about making weekly notes for the different teams I work with at Gusto to read about what the design systems team is working on, things we've fixed, and any bugs we've encountered during that might impact other designers and engineers across our organization. I’ve spent a couple of hours thinking about how we might use Airtable to collect a whole bunch of data and then use its API to extract that info and show it in a web app.

Here’s an example of what we’ll end up building, which is basically a React web app that’s using Airtable as a nifty sorta CMS:

To get started, we have to head on over to the command line and run the following (but first make sure npm is installed):

npx create-react-app airtable-test

This will create a new directory called airtable-test which is where we’ll be making our little React app. If we run yarn start in the command line after that’s finished installing, then we’ll see the default page for the project:

And, if we open up src/App.js in that airtable-test directory, we can see how this page is being rendered with React:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit src/App.js and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          
        </header>
      </div>
    );
  }
}

export default App;

Now that we have React up and running, we can go ahead and install airtable in the command line which will let us interact with the Airtable API:

npm i airtable

Once we’ve done that, we’ll need to create an Airtable account and create a project. We should wind up with something like this spreadsheet:

A screenshot of a spreadsheet. The header is a bright orange and the sheet contains four rows and four columns of content, ordered by date.

Now we can then head to airtable.com/api and select our project so that is serves as data we're pulling from. In this case, I selected “Design Systems Projects” which you can see right at the bottom here:

This will send us to a handy docs website that gives us an incredibly easy to read API for our specific project! Scrolling down we’ll find our API key which we’ll need to access this data as well as a ton of examples that we can use to manipulate the data we get back:

A screenshot of the Airtable API webpage. The heading says Standard API and a list of options, including example projects, is provided. Below the example projects is the project configured in this post, called Design Systems Project.

Let's head back to App.js in our airtable-test directory, delete all the code in that file, and replace it with the following:

import React, { Component } from 'react';
import Airtable from 'airtable';
const base = new Airtable({ apiKey: 'XXXXXXXXXXX' }).base('XXXXXXXXXXX');

Make sure to replace those Xs with the details that you can see in that Airtable API doc we just opened. But now that we’ve done all the setup, we can finally get around to creating our interface by calling data from our spreadsheet.

In App.js we can start to construct the App component:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      records: []
    };
  }
  render() {
    return {
      <div className="App">Hello</div>		}
    }
  }
}

All this will do for now is setup the app’s state and then render “Hello” on the page. Next up, we’ll be add each record from Airtable to that state.

First thing that’s important to note below: in componentDidMount() we’ll be selecting Updates which is just a way of telling Airtable that we want the spreadsheet called Updates. Make sure that this name is the same name as the spreadsheet. We’ll also be looping through all the records, or rows, of our table in that function too:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      records: []
    };
  }
  componentDidMount() {
    base('Updates').select({view: 'Grid view'})
    .eachPage(
      (records, fetchNextPage) => {
        this.setState({
          records
        });
        console.log(records);
        fetchNextPage();
      }
    );
  }
  render() {
    return (
      <div className="App">
        <div>Hello</div>
      </div>
    );
  }
}

fetchNextPage() is the Airtable API’s way of giving us the next record in our spreadsheet and it’s neat that it will keep going until there are no more records. Again, we’re not doing anything with that data yet; we only want to make sure everything is working correctly at this point.

Open up the console in DevTools and we should see something like this:

Array(4) [ {…}, {…}, {…}, {…} ]

And if we dive through each of the objects in this array, then we should find all the data from the spreadsheet! Doing this bit always feels like magic to me.

Anyway, next up we can update our render() function like so:

render() {
  return (
    <div className="App">
      {this.state.records.length > 0 ? (
        this.state.records.map((record, index) =>
          <div key={index}>
            <h2>{record.fields['Date']}</h2>
            {record.fields['UI Kit']}
            {record.fields['Component Library']}
          </div>
        )
        ) : (
          <p>Loading...</p>
        )}
      </div>
    </div>
  );
}

We’re going to be looping through the state that we setup earlier and then rendering the record.fields[] for each column in our spreadsheet. We have a Date, UI Kit, and Component Library column, and once we’ve updated our App.js with the code above, we should see all the content from our spreadsheet!

A screenshot of a default webpage with no styling with four headings containing dates in year/month/day numeric format following by a block of text containing content from the spreadsheet.

It’s like magic! But why does this data look so weird? Well, it’s because I wanted to write Markdown in each cell, so now we’ll need to use a parser to convert that data into good ol’ fashioned HTML. First, we need to head back to the command line though:

npm i showdown 

showdown will help us parse all that Markdown we’ve written in our Airtable spreadsheet. After installing it, we only need to import it at the top of our App.js file, like this:

import showdown from 'showdown';
const markdownConverter = new showdown.Converter();

After the componentDidMount() function, we can create another function that will create our HTML using showdown:

createHTML(markdown){
  return(
    markdownConverter.makeHtml(markdown)
  )
}

It’s a little iffy, but it makes me feel like the code is a bit tidier this way. Now we can update our render() function:

  render() {
  return (
    <div className="App">
      {this.state.records.length > 0 ? (
        this.state.records.map((record, index) =>
          <div key={index}>
            <h2>{new Date(record.fields['Date']).toISOString().split('T', 1)}</h2>
            <div dangerouslySetInnerHTML={{__html: this.createHTML(record.fields['UI Kit'])}} />
            <div dangerouslySetInnerHTML={{__html: this.createHTML(record.fields['Component Library'])}} />
          </div>
        )
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

We’re doing a couple of new things here: we’re using dangerouslySetInnerHTML in each div which, in turn, uses our createHTML function to convert the data from each column (specifically, the UI Kit and Component Library columns). We’re also converting the dates of each row into headings to make things a bit easier to read.

And with that we’re pretty much done! Here’s the final App.js:

import React, { Component } from 'react';
import Airtable from 'airtable';
import showdown from 'showdown';

const markdownConverter = new showdown.Converter();

const base = new Airtable({ apiKey: 'xxxxxxx' }).base('xxxxxxx');

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      records: []
    };
  }
  componentDidMount() {
    base('Updates').select({view: 'Grid view'})
    .eachPage(
      (records, fetchNextPage) => {
        this.setState({
          records
        });
        fetchNextPage();
      }
    );
  }
  createHTML(markdown){
    return(
      markdownConverter.makeHtml(markdown)
    )
  }
  render() {
    return (
      <div className="App">
        {this.state.records.length > 0 ? (
          this.state.records.map((record, index) =>
            <div key={index}>
              <h2>{new Date(record.fields['Date']).toISOString().split('T', 1)}</h2>
              <div dangerouslySetInnerHTML={{__html: this.createHTML(record.fields['UI Kit'])}} />
              <div dangerouslySetInnerHTML={{__html: this.createHTML(record.fields['Component Library'])}} />
            </div>
          )
        ) : (
          <p>Loading...</p>
        )}
      </div>
    );
  }
}

export default App;

There’s still a ton of updates we could make to improve things. I took a first pass at styling, but we probably want to do things like improve the date format and maybe have some kind of indication as to which updates refer to which rows in the spreadsheet. Maybe we could even toggle showing which information to show depending on whether you’re a designer or engineer.

An animating screenshot of the final result which contains the same content as the earlier example but with extra spacing between sections and the column headings in dark orange and flushed to the left of the content instead of above it.

Anyway! I think this is a good start to getting to grips with the Airtable API and I’d love to hear about how you use it in the comments below.

The post Getting to Grips with the Airtable API appeared first on CSS-Tricks.