DEV Community

Ryan Dsouza
Ryan Dsouza

Posted on

Testing API calls

In the second part, we will learn how to test components that fetch data from an API and render that data in the UI.

This is a simple Users component.

import React, { useEffect, useState } from 'react'
import { User } from 'types/users'
import { getUsers } from 'services/users'

const Users: React.FC = () => {
  let [users, setUsers] = useState<User[]>([])
  let [loading, setLoading] = useState(false)

  useEffect(() => {
    setLoading(true)
    getUsers()
      .then(users => setUsers(users))
      .catch(console.error)
      .then(() => setLoading(false))
  }, [])

  return loading ? (
    <p aria-label="loading">Loading ...</p>
  ) : (
    <ul style={{ listStyle: 'none' }}>
      {users.map(user => (
        <li key={user.id}>
          {user.name} ({user.email})
        </li>
      ))}
    </ul>
  )
}

export default Users
Enter fullscreen mode Exit fullscreen mode

Here in the useEffect hook, I have called the getUsers method and set a loading and users state based on when data is received from the API. Depending on that, we set a Loading indicator and after the users are fetched, we render a couple of user details in a list.

Note: If you're not familiar with hooks, then you can replace the useState calls with the initial state you define normally in class components and the useEffect method with componentDidMount.

This is the getUsers method.

export const getUsers = () => {
  return fetch('https://jsonplaceholder.typicode.com/users').then(res =>
    res.json()
  )
}
Enter fullscreen mode Exit fullscreen mode

I am simply using JSONPlaceholder to fetch some fake users. In this test, we will check if the loading text appears and after the API call is made, the users are visible.

Now, your tests should be isolated and so whenever they run, calling the actual API from a server or any 3rd party service, would be both dependent and inefficient which doesn't satisfy the isolation principle. So, we should mock our API request and return a sample response of our own.

So for this, I have used the react-mock package, which provides a handy API for mocking fetch requests.

First, we add the required imports and create a sample users array to be returned.

import React from 'react'
import { render } from '@testing-library/react'
import { FetchMock } from '@react-mock/fetch'
import Users from './Users'
import { User } from 'types/users'

const users: Partial<User>[] = [
  {
    id: 1,
    name: 'Leanne Graham',
    email: 'Sincere@april.biz',
  },
  {
    id: 2,
    name: 'Ervin Howell',
    email: 'Shanna@melissa.tv',
  },
]
Enter fullscreen mode Exit fullscreen mode

Note: Notice something imported apart from render, i.e. waitForElement. This is just the method we need to assert if an element is in the dom after any asynchronous operation.

Second, we create a helper method that uses the FetchMock component to simulate our API.

const renderUsers = () => {
  return render(
    <FetchMock
      matcher="https://jsonplaceholder.typicode.com/users"
      response={users}
    >
      <Users />
    </FetchMock>
  )
}
Enter fullscreen mode Exit fullscreen mode

Here we are providing the url of the api in the matcher prop and the response prop contains the users data that we are mocking.

Note: I have not included all the fields that the API returns but only a subset of the fields specially those that are rendered in the component.

At last, we write our test block as follows.

test(`should render the users list`, async () => {
  const { getByLabelText, findByRole } = renderUsers()
  expect(getByLabelText('loading')).toBeInTheDocument()

  let userList = await findByRole('list')
  expect(userList.children.length).toEqual(users.length)
})
Enter fullscreen mode Exit fullscreen mode

Now this is where it gets interesting.

The first line is simple, rendering the Users component with the FetchMock wrapper to obtain the getByLabelText method to query the component elements.

The second line asserts whether the loading text is being shown in the UI. This is done using the toBeInTheDocument matcher and matched using the aria-label that we have added on the p tag.

Note: toBeInTheDocument is not a native Jest matcher, but is from the library jest-dom. We use this by creating a setupTests.ts file in the src folder and adding this line import '@testing-library/jest-dom/extend-expect'. This will automatically add the DOM matchers that we can use with expect.

The third line is where we use the findByRole method to fetch the list.

let userList = await findByRole('list')
Enter fullscreen mode Exit fullscreen mode

We have used await here because this method returns a Promise and accepts a matcher (in the form of a role) that returns an HTML element. Until our mock API returns a response that we provided, this will wait for the DOM element specified i.e. the ul tag in which we have rendered our users list.

In our component, the loading content is replaced with the users list after the API returns a successful response. So findByRole will check for the element in the DOM until it's available and if it's not, it will throw an error.

As our mock API is a success, findByRole will get the required element i.e. the ul tag.

In the fourth and last line of the test, we assert whether the length of the list rendered is equal to the length of our sample data we passed to the mock API.

expect(userList.children.length).toEqual(users.length)
Enter fullscreen mode Exit fullscreen mode

If you run yarn test or npm test, you will see that your test has passed! Go ahead and run your application in the browser with yarn start or npm start and see the loading indicator for a short while and then the users being rendered.

The respository with the example above is here. It includes the example from the previous post in this series and will include the examples for further use cases as well.

Note: As Kent has mentioned in the comments, we can add another line to our test to ensure that the ul has the rendered the users with their emails correctly and that assures us that whatever we have passed as our user list gets rendered.

For this, there is a type of snapshot in jest, inline snapshots! Inline snapshots unlike external snapshots write directly to your test the content that is being rendered instead of creating external .snap files and for that you just need to add this line of code to your test.

expect(userList.textContent).toMatchInlineSnapshot()
Enter fullscreen mode Exit fullscreen mode

Jest will automatically fill the content of the ul tag in the toMatchInlineSnapshot method. So after you save the test, it should be updated with the list you have passed. Sweet right!

Go ahead and change the sample users list we have passed, save the file and notice the changes reflected in this method.

If you're getting a failing test, press u to update the snapshot so that it gets the latest changes that you have made to the users list.

Thank you for reading!

Top comments (8)

Collapse
 
kentcdodds profile image
Kent C. Dodds

Sweet!

You can get rid of the loader test ID by using getByText(/loading/i). And if it's a spinner instead, then you can use aria-label="loading" and getByLabel(/loading/i)

Collapse
 
kentcdodds profile image
Kent C. Dodds

Oh, also, you can improve this as well:

let userList = await waitForElement(() => getByRole('list'))

Change that to this:

let userList = await findByRole('list')
Collapse
 
kentcdodds profile image
Kent C. Dodds

And finally, you may like to do this for your assertion:

expect(userList.textContent).toMatchInlineSnapshot()

Jest will auto-update that code to have an inline snapshot for you and you'll get a tiny bit more confidence that it's rendering what it should (without the implementation details of how that text appears on the screen).

Thread Thread
 
ryands17 profile image
Ryan Dsouza

Inline snapshots are great! That's one new arsenal in my tooklit.

Collapse
 
ryands17 profile image
Ryan Dsouza

Yes that will make it more accessible as well! Thanks a lot :)

Collapse
 
davidariass profile image
David V Arias

Great article on API testing! If this interests you, check out Apidog. The no-code platform for API testing has really simplified my workflow. Perfect for focusing efforts on what matters.

Collapse
 
avic75 profile image
Avi Cohen

I would recommend getMizu.io and open source also available on GitHub github.com/up9inc/mizu

A simple-yet-powerful API traffic viewer for Kubernetes enabling you to view all API communication between microservices to help your debug and troubleshoot regressions.

Think TCPDump and Wireshark re-invented for Kubernetes.

Collapse
 
davidariass profile image
David V Arias

Hey fellow devs, if you're digging into API testing, give Apidog a whirl. It's simplified my testing workflow big time, plus no coding needed!