Test-Driven Development Tutorial with React Testing Library, Jest & GraphQL
Tags: react, graphql, javascript, testing
Published: 2021-07-29 10:51:51

We’re going to build a dumb joke book app, with Test-Driven Development (TDD). I’m going to use a Snowpack toolchain with help from my [React Snowpack QuickStart](https://github.com/NeilWkz/React-Snowpack-Quick-Start). If you want to follow along, the tooling doesn’t matter, so feel free to use Create React App if you prefer. If you want the finished tutorial code, it is available to clone from this [Github Repo](https://github.com/NeilWkz/react-jk-jk)

*Before we start you should be aware that this is a tutorial to demonstrate TDD methodology in a React application, not to teach you how to build a joke fetching app. The technology choices used in this article are unsuitable for a small content marketing app. It would be a performance blunder to load the React framework unless already required for a critical path elsewhere within your platform. A more appropriate choice for a content marketing app would be vanilla js, alpine-js or Svelte. Please also be aware that apollo-client is also a chunky dependency, and again if you’re working on a platform that can be warranted, but if you want a lightweight graphQL client consider graphql-request*

## Start your Tooling
Open the vscode terminal and split-screen it:
![open-splitscreen-1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fwca5znylj6yl5989msv.jpeg)
 

In one terminal start snow pack by running
```
npm start
```
and in the other  terminal start jest in --watch mode by running
```
npm run jest
```

### Create your first test
We’re going to create a tests folder and add a new file called `App.test.tsx`. First, we’ll need to import the basic dependencies we need to test React components

```javascript
import * as React from 'react';
import App from '../App'
import { render} from '@testing-library/react';
```
Our first test will be to make sure that our app has a heading. It’s a basic accessibility & seo requirement.

```javascript
test('The document must have an heading', () => {
  const { getByRole} = render(<App />);
  expect(getByRole('heading')).toBeTruthy();
```
*SIDENOTE: We want the test to be as simple a statement of what the app is doing as possible. In Behaviour-driven development, we would use our Gherkin Scenario*

The test fails! We have Red. Now the core of TDD is getting it to turn Green. We call this RED-GREEN-REFACTOR.

Now we add an h1 to our App.tsx

```javascript
import * as React from 'react'
interface Props {}
export default function App({}: Props) {
  return (
    <div className="container">
      <h1>React Jk-Jk</h1>
    </div>
  )
}
 
```
![Screenshot-2021-04-20-at-07.52.50](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y8els6utyeqg26ix6mv2.png)

The test passes! We have Green. ONWARDS to fail once more, for our next test, we know that we need a button.

```javascript
test('When the app loads there is a button', () => {
    const { getByRole} = render(
    <App />,
  );
  expect(getByRole('button')).toBeTruthy()
})
```

*Wait*, we’ve repeated the render method for our first test. We should share that between our tests. So our test file becomes:

```javascript
const renderApp = () => render(<App />)
test('The document should have an heading', () => {
  const { getByRole } = renderApp()
  expect(getByRole('heading')).toBeTruthy()
})
test('The app has a button', () => {
  const { getByRole } = renderApp()
  expect(getByRole('button')).toBeTruthy()
})
```
Adding a button makes us green, but we need our next test. Given a button, when the user clicks the button then a joke appears.

```javascript
test('When the user clicks the button then a joke appears', () => {
  const testJoke = "What's brown and sticky? ... A stick"
  const { getByRole, getByText } = renderApp(testJoke)
  const button = getByRole('button')
  fireEvent.click(button)
  expect(getByText(testJoke)).toBeInTheDocument()
})
```

You may think that to make this test pass we’d need to go and fetch data from the server, we’d need to work out how we were going to display it, but that’s not true. We’re going to make the test pass in the dumbest way possible

In `App.tsx`:

```javascript
import * as React from 'react'
const { useState } = React
interface Props {
  joke?: string
}
export default function App({joke}: Props) {
  const [isClicked, setIsClicked] = useState(false)
  return (
    <div className="container">
      <h1>React Jk-Jk</h1>
      {isClicked && <p>{joke}</p>}
      <button onClick={()=> setIsClicked(true)}>Click me</button>
    </div>
  )
}
```

Notice we make the component accept a prop joke so it can receive the text, and we then use a useState to determine if the button has been clicked. That passes, but now we must *refactor*.

Let’s get some data
```
npm install @apollo/client graphql
```

This testing data requires a short explanation of a technique that is the basis of most testing: *Mocking*. When we mock API data we are providing our component with data that will not change, so we can be sure that we are testing our component in isolation. Mocking with React Context means that we need to create a test wrapper. Thankfully `apollo-client` comes with its own `mockedProvider` that makes this easy.

```javascript
import { MockedProvider } from '@apollo/client/testing'
const mocks = []
const renderApp = (joke?: string) => {
  return render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <App joke={joke} />
    </MockedProvider>
  )
}
```

In the next part, we need to generate our mock. I’m going to use the `https://icanhazdadjoke.com/api` as a data source, and the insomnia app to grab my mock.

![A screenshot of the insomnia app, showing fetching a graphQL query](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sq9i1haufyz55acthp63.png)

ASIDE: I’m using the graphQL endpoint for demo purposes, to get that to work locally would cause CORS issues. Now CORS issues are why we work with Backend Developers, professionally I’d slack a colleague to sort out the CORS policy, here I’m using the [allow CORS chrome extension](https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf/related?hl=en) to enable CORS locally.

In insomnia we can construct a graphql query and hit the endpoint

```javascript
query joke {
  joke {
    id
    joke
    permalink
  }
}
```

The data returned in Insomnia can form the basis of the mock that we pass to mockedProvider. we give our query the name of `GET_JOKE_QUERY`.

```javascript
const mocks = [
  {
    request: {
      query: GET_JOKE_QUERY,
    },
    result: {
      data: {
        joke: {
          __typename: 'Joke',
          id: 'sPfqWDlq4Ed',
          joke: '"Hey, dad, did you get a haircut?" "No, I got them all cut."',
          permalink: 'https://icanhazdadjoke.com/j/sPfqWDlq4Ed',
        },
      },
    },
  },
]
```
The first state that we’ll test is loading so we’ll write the following test:

```javascript
test('When fetching data the user is shown a loading message', () => {
  const { getByText } = renderApp()
  expect(getByText('Loading...')).toBeInTheDocument()
})
```
Now we’re going to wire up our data plumbing with graphQL, first in index.tsx we set up apollo-client

```javascript
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { ApolloProvider } from '@apollo/client/react'
const uri = 'https://icanhazdadjoke.com/graphql'
const client = new ApolloClient({
  // link: authLink.concat(httpLink),
  uri,
  cache: new InMemoryCache(),
})
var mountNode = document.getElementById('app')
ReactDOM.render(
  <ApolloProvider client={client}>
    <App joke="What's brown and sticky? ... A stick" />
  </ApolloProvider>,
  mountNode
)
```

Now back in our `App.tsx` we import useQuery and add our `GET_JOKE_QUERY` to the head of the file

```javascript
import { useQuery, gql } from '@apollo/client'
export const GET_JOKE_QUERY = gql`
  query joke {
    joke {
      id
      joke
      permalink
    }
  }
```
Then in the body of the component, we destructure loading from useQuery and add an early return with a loading message.

```javascript
export default function App({ joke }: Props) {
  const [isClicked, setIsClicked] = useState(false)
  const { loading } = useQuery(GET_JOKE_QUERY)
  if (loading) return <p>Loading...</p>
  return (
    <div className="container">
      <h1>React Jk-Jk</h1>
      {isClicked && <p>{joke}</p>}
      <button onClick={() => setIsClicked(true)}>Click me</button>
    </div>
  )
}
```
![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fz8m5uszlrsvd9qkgmje.png)
Great 🙂 now our loading test passes, but now all of our other tests fail, we need to make our other tests asynchronous and introduce async-await. We can update our other tests to be:

```javascript
test('The document should have an heading', async () => {
  const { getByRole, getByText } = renderApp()
  await waitForElementToBeRemoved(() => getByText(/Loading.../i))
  expect(getByRole('heading')).toBeTruthy()
})
test('The app has a button', async () => {
  const { getByRole, getByText } = renderApp()
  await waitForElementToBeRemoved(() => getByText(/Loading.../i))
  expect(getByRole('button')).toBeTruthy()
})
test('When the user clicks the button then a joke appears', async () => {
  const testJoke = "What's brown and sticky? ... A stick"
  const { getByRole, getByText } = renderApp(testJoke)
  await waitForElementToBeRemoved(() => getByText(/Loading.../i))
  const button = getByRole('button')
  fireEvent.click(button)
  expect(getByText(testJoke)).toBeInTheDocument()
})
```
That’s good that all 4 tests are GREEN, and passing, but that’s 3 repetitions so we need to refactor that into a helper. I’m not necessarily a DRY (don’t repeat yourself) programmer – I prefer a WET approach (write everything twice so save on hasty abstractions). I’m going to do two things, I’m going to import the screen method from Testing Library, and then I’m going to consolidate those awaits into a helper function.

```javascript
`import { render, screen, fireEvent, waitForElementToBeRemoved } from '@testing-library/react'
```
Then the helper:

```javascript
const doneLoading = (screen: { getByText: (arg0: RegExp) => any }) =>
  waitForElementToBeRemoved(() => screen.getByText(/Loading.../i))
```
So it has the benefit of making our tests a bit more readable:

```javascript
test('The document should have an heading', async () => {
  renderApp()
  await doneLoading(screen)
  expect(screen.getByRole('heading')).toBeTruthy()
})
test('The app has a button', async () => {
  renderApp()
  await doneLoading(screen)
  expect(screen.getByRole('button')).toBeTruthy()
})
test('When the user clicks the button then a joke appears', async () => {
  const testJoke = "What's brown and sticky? ... A stick"
  renderApp(testJoke)
  await doneLoading(screen)
  const button = screen.getByRole('button')
  fireEvent.click(button)
  expect(screen.getByText(testJoke)).toBeInTheDocument()
})
```
Now we want to change the behaviour so that the app loads and then fetches data and then shows us a joke so we write:

```javascript
test("When data is fetched a joke is displayed on the screen", async ()=> {
  renderApp()
  await doneLoading(screen)
  expect(screen.getByTestId('joke')).toBeInTheDocument()
})
```
So the fastest way to make that green is to simply add a `test-id` to our App.tsx
```javascript
return (
        data ? (
            <div className="container">
                <h1>React Jk-Jk</h1>
                <p data-testid="joke">{JSON.stringify(data)}</p>
                {isClicked && <p>{joke}</p>}
                <button onClick={() => setIsClicked(true)}>Click me</button>
            </div>
        ) : null
    )
}
```
We need to refactor to get the behaviour we want. We’re going to need to actually display a joke.
So we’re going to create a small component to display a joke.

```javascript
import * as React from 'react'
interface Joke {
  id: string
  joke: string
  permalink: string
}
export default function Joke(jokeData: Joke) {
  return (
    <div>
      <p>{jokeData.joke}</p>
    </div>
  )
}
```

Now we have a failing test we need to refactor our _*“When the user clicks the button then a joke appears”*_ test. We’re going to change this to be _*“When the user clicks the button the app fetches a new joke”*_. We refactor our spec:
```javascript
test("When the user clicks the button the app fetches a new joke", async () => {
    renderApp()
    await screen.findByTestId("joke")
    const button = screen.getByRole("button")
    fireEvent.click(button)
    await screen.findByTestId("joke")
    expect(mockJokes).toHaveBeenCalledTimes(2)
})
```
You’ll notice that instead of awaiting our doneLoading function we are now awaiting a joke appearing on the screen, then clicking our button and then awaiting another joke. Our expect statement now introduces another key concept of testing, mocking. So let’s write our mock.

To make this test go green, we need to get some more results from our service and store them in our mock. Now we create an array of only the results
```javascript
const jokes = [
  {
      data: {
          joke: {
              id: "39Etc2orc",
              joke:
                  "Why did the man run around his bed? Because he was trying to catch up on his sleep!",
              permalink: "https://icanhazdadjoke.com/j/39Etc2orc",
              __typename: "Joke",
          },
      },
  },
  {
      data: {
          joke: {
              __typename: "Joke",
              id: "sPfqWDlq4Ed",
              joke:
                  '"Hey, dad, did you get a haircut?" "No, I got them all cut."',
              permalink: "https://icanhazdadjoke.com/j/sPfqWDlq4Ed",
          },
      },
  },
  {
      data: {
          joke: {
              id: "wcxHJBl3gFd",
              joke:
                  "I am terrified of elevators. I\u2019m going to start taking steps to avoid them.",
              permalink: "https://icanhazdadjoke.com/j/wcxHJBl3gFd",
              __typename: "Joke",
          },
      },
  },
]
```
Then we need to make the mockedProvider request different jokes:

```javascript
const mocks = [
    {
        request: {
            query: GET_JOKE_QUERY,
        },
        result: () => mocks[0],
        newData: () => mocks[1],
    },
]
```
We could test the `screen.findByTestId("joke").content` and then click the button and test that the content had changed, but we’re trying to test that the button has called the apollo client’s refetch method. We go a step further and create a jest function to return the data.
```javascript
const mockJokes = jest
    .fn()
    .mockReturnValue(jokes[0])
    .mockReturnValueOnce(jokes[1])
    .mockReturnValueOnce(jokes[2])
const mocks = [
    {
        request: {
            query: GET_JOKE_QUERY,
        },
        result: () => mockJokes(),
        newData: () => mockJokes(),
    },
]
beforeEach(() => mockJokes.mockClear())
```
The `jest.fn()` method is so important to the process of testing. It’s likely that if we’re struggling to test something, we need to take a step back and refocus on the way in which we’re mocking external dependencies. We’re using the mockReturnValue to set default data, then we’re making the function return a different data object from our array of mocks each time the function is called with mockReturnValueOnce. Importantly, because our expect is `expect(mockJokes).toHaveBeenCalledTimes(2)` we need to add jest’s beforeEach hook to reset the mock before each test, otherwise the mock will persist, and for each test in the `App.test.tsx` it would run, meaning that by the time it reached our test it could be called 4 times, and when another developer in the future inserted new test before it would break our test.
So now we’ve refactored our test all that remains is to update our component to make it green.
In our `App.tsx` we update useQuery to destructure the refetch method, and then we update our `onClick` function to call `refetch()`.

```javascript
export default function App() {
    const { loading, data, refetch } = useQuery(GET_JOKE_QUERY)
    if (loading) {
        return <p>Loading...</p>
    }
    return (
        <div className="container">
            <h1>React Jk-Jk</h1>
            {data && <Joke joke={data.joke.joke} id={data.joke.id} />}
            <button onClick={() => refetch()}>Click me</button>
        </div>
    )
}
```
And we’re done with the test-driven development. We’ve met the behaviour required. I intend to post another tutorial demonstrating how I would style the joke book app, because TDD may allow you to deploy on Fridays and sleep soundly, but nothing is production-ready until it looks good enough for users to want to use it. I’ll update this page with a link when I write that tutorial.

If you have been, *thanks for following along*. I welcome any comments or feedback on this article.

### Acknowledgements
My thanks go to [Brett Langdon](https://brett.is/) the maintainer of icanhazdadjoke, this wouldn’t have been possible without an API. I take inspiration for this tutorial from [this excellent article on TDD](https://medium.com/magnetis-backstage/working-an-application-in-vue-js-with-tdd-an-extensive-guide-for-people-who-have-time-part-1-3be791dafa2b) with Vue from [Daniel Kuroski](https://github.com/kuroski) that helped me get into TDD in 2018. When I was working with Vue, I was immensely grateful for his comprehensive tutorial, my hope is that this tutorial can be as useful to a React Developer interested in getting started with TDD.