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
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
Yarn
yarn add -D jest babel-jest @testing-library/jest-dom @testing-library/react babel-preset-gatsby identity-obj-proxy
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
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)
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"
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"
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(),
}
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"],
}
This file can look confusing at first, but it's relatively easy to grasp. Let's break it down:
-
transform
use Babel to compile the JSX code. -
moduleNameMapper
mock the static assets. -
testPathIgnorePatterns
exclude the folders listed in the array when running the tests. -
transformIgnorePatterns
exclude the folders listed in the array when transforming the JSX code. -
globals
indicate to Jest the folders to test. -
setupFilesAfterEnv
importjest-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"
}
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
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
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)
})
})
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:
- It tests if the
title
from the metadata is equal toAll posts | Gatsby Starter Blog
. - It checks if the
description
from the metadata is equal toA starter blog demonstrating what Gatsby can do.
. - It tests if the
twitter
from the metadata is equal tokylemathews
. - 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
Or, for yarn
yarn test
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
}
}
}
}
`
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)
})
})
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:
- It tests if the title of the
Link
component of the first article is equal toMy awesome first blog post
- It test if the description of the second article is equal to
My awesome second blog description
. - It tests if the array of articles is equal to 2.
Now, execute this command on the CLI.
npm test
Or, for yarn
yarn test
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.