Unit Testing Your Gatsby Site with Jest and React Testing library

Ibrahima Ndaw - Jan 21 '21 - - Dev Community

Testing is a crucial piece when it comes to building websites or apps. It gives you more confidence in your product, makes your code better, and helps to avoid unexpected bugs in production.

In this tutorial, we will introduce you to unit testing by showing you how to test your Gatsby site with Jest and the React Testing Library. Let's get started.

Creating a new Gatsby app

To keep us focused on testing, we’ll use an out of the box Gatsby starter template. To use a Gatsby starter, begin by opening your command-line interface (CLI) and run this command:

npx gatsby new my-blog-starter https://github.com/gatsbyjs/gatsby-starter-blog
Enter fullscreen mode Exit fullscreen mode

Note - If you have the Gatsby CLI installed on your machine, you can omit *npx*.

Next we will set up the testing environment. Unlike React, Gatsby does not ship with Jest or React Testing Library, so we’ll install those now.

Setting up our testing environment

Execute the command on the CLI to install the libraries needed for testing a Gatsby site.

NPM

npm install -D jest babel-jest @testing-library/jest-dom @testing-library/react babel-preset-gatsby identity-obj-proxy
Enter fullscreen mode Exit fullscreen mode

Yarn

yarn add -D jest babel-jest @testing-library/jest-dom @testing-library/react babel-preset-gatsby identity-obj-proxy
Enter fullscreen mode Exit fullscreen mode

With the dependencies installed, we can now create a new folder (tests) at the root of the project. The directory structure should look like this:

    tests
    ├── jest-preprocess.js
    ├── setup-test-env.js
    └── __mocks__
        ├── file-mock.js
        └── gatsby.js
Enter fullscreen mode Exit fullscreen mode

With the folder structure in place, next we configure Jest. Let's begin with jest-preprocess.js

    // tests/jest-preprocess.js
    const babelOptions = {
      presets: ["babel-preset-gatsby"],
    }
    module.exports = require("babel-jest").createTransformer(babelOptions)
Enter fullscreen mode Exit fullscreen mode

This config tells Gatsby how to compile our tests with Babel because both Gatsby and Jest use modern JavaScript and JSX.

    // tests/setup-test-env.js
    import "@testing-library/jest-dom/extend-expect"
Enter fullscreen mode Exit fullscreen mode

As you can see, this file allows us to import jest-dom in one place and then use it on every test file.

    // tests/__mocks__/file-mock.js
    module.exports = "test-file-stub"
Enter fullscreen mode Exit fullscreen mode

If you need to mock static assets, then this file is required to do so. We won't have that use-case, but be aware of what it does.

    // tests/__mocks__/gatsby.js
    const React = require("react")
    const gatsby = jest.requireActual("gatsby")
    module.exports = {
      ...gatsby,
      graphql: jest.fn(),
      Link: jest.fn().mockImplementation(
        // these props are invalid for an `a` tag
        ({
          activeClassName,
          activeStyle,
          getProps,
          innerRef,
          partiallyActive,
          ref,
          replace,
          to,
          ...rest
        }) =>
          React.createElement("a", {
            ...rest,
            href: to,
          })
      ),
      StaticQuery: jest.fn(),
      useStaticQuery: jest.fn(),
    }
Enter fullscreen mode Exit fullscreen mode

This file allows us to mock some Gatsby features in order to query data with GraphQL or use the Link component. Make sure to name the folder __mocks__ and the file gatsby.js — otherwise, Gatsby will throw errors.

With this configuration in place, we can dive into Jest and customize it to follow our needs. Let's begin by creating a jest.config.js file in the root of the project and then add this code below.

    // jest.config.js
    module.exports = {
      transform: {
        "^.+\\.jsx?$": `<rootDir>/tests/jest-preprocess.js`,
      },
      moduleNameMapper: {
        ".+\\.(css|styl|less|sass|scss)$": `identity-obj-proxy`,
        ".+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": `<rootDir>/tests/__mocks__/file-mock.js`,
      },
      testPathIgnorePatterns: [`node_modules`, `\\.cache`, `<rootDir>.*/public`],
      transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`],
      globals: {
        __PATH_PREFIX__: ``,
      },
      setupFilesAfterEnv: ["<rootDir>/tests/setup-test-env.js"],
    }
Enter fullscreen mode Exit fullscreen mode

This file can look confusing at first, but it's relatively easy to grasp. Let's break it down:

  1. transform use Babel to compile the JSX code.
  2. moduleNameMapper mock the static assets.
  3. testPathIgnorePatterns exclude the folders listed in the array when running the tests.
  4. transformIgnorePatterns exclude the folders listed in the array when transforming the JSX code.
  5. globals indicate to Jest the folders to test.
  6. setupFilesAfterEnv import jest-dom before test runs.

The last step of the config consists of tweaking the package.json file to run Jest with the CLI.

    // package.json
      "scripts": {
        "test": "jest"
      }
Enter fullscreen mode Exit fullscreen mode

Phew! The setting up is complete. Let's now start writing our tests in the next section.

Writing the unit tests

Unit testing is a method that ensures that a section of an application behaves as intended.
In this article, we will be testing the SEO component and the Home page (index.js). Let's structure the folder as follows:

    src
    ├── components
    | ├── seo.js
    | ├── bio.js
    | ├── layout.js
    | └── __tests__
    | |    |   └── seo.js
    ├── pages
    | ├── 404.js
    | ├── index.js
    | └── __tests__
    | |    |   └── index.js
Enter fullscreen mode Exit fullscreen mode

You can use .spec or .test to create a testing file or put the files in the __tests__ folder. Jest will only execute the files under the __tests__ folder.

    // components/seo.js
    import React from "react"
    import PropTypes from "prop-types"
    import { Helmet } from "react-helmet"
    import { useStaticQuery, graphql } from "gatsby"

    const SEO = ({ description, lang, meta, title }) => {
      const { site } = useStaticQuery(
        graphql`
          query {
            site {
              siteMetadata {
                title
                description
                social {
                  twitter
                }
              }
            }
          }
        `
      )

      const metaDescription = description || site.siteMetadata.description
      const defaultTitle = site.siteMetadata?.title

      return (
        <Helmet
          htmlAttributes={{
            lang,
          }}
          title={title}
          titleTemplate={defaultTitle ? `%s | ${defaultTitle}` : null}
          meta={[
            {
              name: `description`,
              content: metaDescription,
            },
            {
              property: `og:title`,
              content: title,
            },
            {
              property: `og:description`,
              content: metaDescription,
            },
            {
              property: `og:type`,
              content: `website`,
            },
            {
              name: `twitter:card`,
              content: `summary`,
            },
            {
              name: `twitter:creator`,
              content: site.siteMetadata?.social?.twitter || ``,
            },
            {
              name: `twitter:title`,
              content: title,
            },
            {
              name: `twitter:description`,
              content: metaDescription,
            },
          ].concat(meta)}
        />
      )
    }

    SEO.defaultProps = {
      lang: `en`,
      meta: [],
      description: ``,
    }

    SEO.propTypes = {
      description: PropTypes.string,
      lang: PropTypes.string,
      meta: PropTypes.arrayOf(PropTypes.object),
      title: PropTypes.string.isRequired,
    }

    export default SEO
Enter fullscreen mode Exit fullscreen mode

Now, let's write the unit tests for the SEO component.

    // components/__tests__/seo.js
    import React from "react"
    import { render } from "@testing-library/react"
    import { useStaticQuery } from "gatsby"

    import Helmet from "react-helmet"
    import SEO from "../seo"

    describe("SEO component", () => {
      beforeAll(() => {
        useStaticQuery.mockReturnValue({
          site: {
            siteMetadata: {
              title: `Gatsby Starter Blog`,
              description: `A starter blog demonstrating what Gatsby can do.`,
              social: {
                twitter: `kylemathews`,
              },
            },
          },
        })
      })

      it("renders the tests correctly", () => {
        const mockTitle = "All posts | Gatsby Starter Blog"
        const mockDescription = "A starter blog demonstrating what Gatsby can do."
        const mockTwitterHandler = "kylemathews"

        render(<SEO title="All posts" />)
        const { title, metaTags } = Helmet.peek()

        expect(title).toBe(mockTitle)
        expect(metaTags[0].content).toBe(mockDescription)
        expect(metaTags[5].content).toBe(mockTwitterHandler)
        expect(metaTags.length).toBe(8)
      })
    })
Enter fullscreen mode Exit fullscreen mode

We start by importing React Testing Library, which allows rendering the component and giving access to DOM elements. After that, we mock the GraphQL query with useStaticQuery to provide the data to the SEO component.

Next, we rely on the render method to render the component and pass in the title as props. With this, we can use Helmet.peek() to pull the metadata from the mocked GraphQL query.

Finally, we have four test cases:

  1. It tests if the title from the metadata is equal to All posts | Gatsby Starter Blog.
  2. It checks if the description from the metadata is equal to A starter blog demonstrating what Gatsby can do..
  3. It tests if the twitter from the metadata is equal to kylemathews.
  4. It checks if the length of the metaTags array is equal to 8.

To run the tests, we have to execute this command on the CLI.

npm test
Enter fullscreen mode Exit fullscreen mode

Or, for yarn

yarn test
Enter fullscreen mode Exit fullscreen mode

All tests should pass as expected by showing some nice green sticks on the CLI.

    // pages/index.js

    import React from "react"
    import { Link, graphql } from "gatsby"

    import Bio from "../components/bio"
    import Layout from "../components/layout"
    import SEO from "../components/seo"

    const BlogIndex = ({ data, location }) => {
      const siteTitle = data.site.siteMetadata?.title || `Title`
      const posts = data.allMarkdownRemark.nodes
      if (posts.length === 0) {
        return (
          <Layout location={location} title={siteTitle}>
            <SEO title="All posts" />
            <Bio />
            <p>
              No blog posts found. Add markdown posts to "content/blog" (or the
              directory you specified for the "gatsby-source-filesystem" plugin in
              gatsby-config.js).
            </p>
          </Layout>
        )
      }

      return (
        <Layout location={location} title={siteTitle}>
          <SEO title="All posts" />
          <Bio />
          <ol style={{ listStyle: `none` }}>
            {posts.map(post => {
              const title = post.frontmatter.title || post.fields.slug
              return (
                <li key={post.fields.slug}>
                  <article
                    className="post-list-item"
                    itemScope
                    itemType="http://schema.org/Article"
                  >
                    <header>
                      <h2>
                        <Link
                          data-testid={post.fields.slug + "-link"}
                          to={post.fields.slug}
                          itemProp="url"
                        >
                          <span itemProp="headline">{title}</span>
                        </Link>
                      </h2>
                      <small>{post.frontmatter.date}</small>
                    </header>
                    <section>
                      <p
                        data-testid={post.fields.slug + "-desc"}
                        dangerouslySetInnerHTML={{
                          __html: post.frontmatter.description || post.excerpt,
                        }}
                        itemProp="description"
                      />
                    </section>
                  </article>
                </li>
              )
            })}
          </ol>
        </Layout>
      )
    }

    export default BlogIndex
    export const pageQuery = graphql`
      query {
        site {
          siteMetadata {
            title
          }
        }
        allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
          nodes {
            excerpt
            fields {
              slug
            }
            frontmatter {
              date(formatString: "MMMM DD, YYYY")
              title
              description
            }
          }
        }
      }
    `
Enter fullscreen mode Exit fullscreen mode

Notice that here, we use data-testid on some elements to be able to select them from the testing file. Let's write the unit tests for the home page.

    //pages/__tests__/index.js
    import React from "react"
    import { render } from "@testing-library/react"
    import { useStaticQuery } from "gatsby"

    import BlogIndex from "../index"

    describe("BlogIndex component", () => {
      beforeEach(() => {
        useStaticQuery.mockReturnValue({
          site: {
            siteMetadata: {
              title: `Gatsby Starter Blog`,
              description: `A starter blog demonstrating what Gatsby can do.`,
              social: {
                twitter: `kylemathews`,
              },
            },
          },
        })
      })
      it("renders the tests correctly", async () => {
        const mockData = {
          site: {
            siteMetadata: {
              author: "John Doe",
            },
          },
          allMarkdownRemark: {
            nodes: [
              {
                excerpt: "This is my first excerpt",
                fields: {
                  slug: "first-slug",
                },
                frontmatter: {
                  date: "Nov 11, 2020",
                  title: "My awesome first blog post",
                  description: "My awesome first blog description",
                },
              },
              {
                excerpt: "This is my second excerpt",
                fields: {
                  slug: "second-slug",
                },
                frontmatter: {
                  date: "Nov 12, 2020",
                  title: "My awesome second blog post",
                  description: "My awesome second blog description",
                },
              },
            ],
          },
        }

        const { getByTestId } = render(
          <BlogIndex data={mockData} location={window.location} />
        )

        const { nodes } = mockData.allMarkdownRemark
        const post1 = "first-slug-link"
        const post2 = "second-slug-desc"

        expect(getByTestId(post1)).toHaveTextContent(nodes[0].frontmatter.title)
        expect(getByTestId(post2)).toHaveTextContent(
          nodes[1].frontmatter.description
        )
        expect(nodes.length).toEqual(2)
      })
    })
Enter fullscreen mode Exit fullscreen mode

As you can see, we start by mocking the GraphQL query. Next, we create the dummy data and then pass in the object to the BlogIndex component.

After that, we pull out getTestId from the render method, which enables us to select elements from the DOM. With this in place, we can now explain what the tests do:

  1. It tests if the title of the Link component of the first article is equal to My awesome first blog post
  2. It test if the description of the second article is equal to My awesome second blog description.
  3. It tests if the array of articles is equal to 2.

Now, execute this command on the CLI.

npm test
Enter fullscreen mode Exit fullscreen mode

Or, for yarn

yarn test
Enter fullscreen mode Exit fullscreen mode

All unit tests should pass.
With this final step, we are now able to test our Gatsby site with Jest and React Testing Library. You can find the finished project in this Github repo.

Conclusion

In this tutorial, we learned how to test a Gatsby site with Jest and React Testing Library. Testing is often seen as a tedious process, but the more you dig into it, the more value you get on your app.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .