Testing Gatsby

Testing Gatsby

3 min read
GatsbyTestsJest

One of the first steps in developing this site after setting up the environment was to configure the VS Code launch.json to be able to debug during development and remove bugs more easily. Gatsby documentation provides a full configuration for my purpose for both develop and build phases.

json
{
"version": "0.2.0",
"configurations": [
{
"name": "Gatsby develop",
"type": "node",
"request": "launch",
"protocol": "inspector",
"program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",
"args": ["develop"],
"stopOnEntry": false,
"runtimeArgs": ["--nolazy"],
"sourceMaps": false
},
{
"name": "Gatsby build",
"type": "node",
"request": "launch",
"protocol": "inspector",
"program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",
"args": ["build"],
"stopOnEntry": false,
"runtimeArgs": ["--nolazy"],
"sourceMaps": false
}
]
}

After developing some basic components, I noticed that I was only focusing on adding features and new code, so I got an idea: unit test! What could I do? In my experience as a backend developer, unit testing is a good habit like an espresso for breakfast (yes, I know, unit testing always adds time to the development so it might depend on the project, client, change requests, delivery time, but that's another story...) and the goal is to get good coverage.

So, I would like to write about what I used to achieve my purpose of doing unit testing on React components with a particular focus on a series of useful tests. There are many things online, I have tried to understand the possible solutions according to the cases and apply them. In the end, I used Jest and React-testing-library. As a further section to the initial launch.json, I needed to add a new configuration to be able to debug also tests, below is what it did for me:

json
{
"version": "0.2.0",
"configurations": [
[...]
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
}
]
}

However, I won't cover the basic Jest configuration because it's deeply explained in the documentation and it is quite simple, I have followed what is described and added the following core files: jest-preprocess.js, jest.config.js, loadershim.js.

Footer

I started from the footer because it's one of the simplest site's components. Here there's no great logic, there are static informations, some text, icons. So tests will not be extraordinary but it was a good starting point to gain confidence.

js
// footer.js
import React from "react"
import socialLinks from "../constants/social_links"
import { FaRegHeart } from 'react-icons/fa'
const Footer = () => {
return (
<footer className="footer">
<div className="foot-center">
<div className="foot-header">
<h4>
&copy;{new Date().getFullYear()}. Built with <FaRegHeart/> <a href="https://www.gatsbyjs.org" title="Gatsby">
Gatsby
</a>
</h4>
</div>
<div className="foot-links">
{socialLinks.map(link => {
return (
<a href={link.url} key={link.id} className="social-icon" aria-label={link.name}>
{link.icon}
</a>
)
})}
</div>
</div>
</footer>
)
}
export default Footer

I wanted to check the complete rendering of the component and particularly the elements within the social link array. So in the first case, I used the render() function from react-testing-library in conjunction with Jest toMatchSnapshot(). In the second test, I used the DOM querySelector() method to get my social link elements and Jest with its toHaveLength() control, to verify that it is set to the right numeric value.

js
//footer.spec.js
import React from "react"
import { render } from "@testing-library/react"
import Footer from "./Footer"
describe("Footer", () => {
it("renders correctly", () => {
const container = render(<Footer />)
expect(container).toMatchSnapshot()
})
it("Should have array of social Links", () => {
const {container} = render(<Footer />)
const elements = container.querySelector('.foot-links')
expect(elements.childNodes).toHaveLength(3)
})
})

Pagination

Pagination is a component that receives some props, will allow users to simply navigate to the previous/next page. In the "All Posts" page of this site there are four articles at a time and each page will query GraphQL for those specific items.

js
// pagination.js
import React from "react"
import { Link } from 'gatsby'
import { FaArrowRight, FaArrowLeft } from 'react-icons/fa'
const Pagination = ({isFirst, isLast, prevPage, nextPage}) => {
return (
<div className="paginated-info">
{!isFirst && (
<Link to={`/blog/${prevPage}`} rel="prev">
<FaArrowLeft className="post-arrow-link"/> {"Previous Page"}
</Link>
)}
{!isLast && (
<Link to={`/blog/${nextPage}`} rel="next">
{"Next Page"} <FaArrowRight className="post-arrow-link"/>
</Link>
)}
</div>
)
}
export default Pagination

Here, I have decided to do three types of tests, on first, middle and final page. Each test calls the render() function using the props that represent these scenarios, then verifies the presence or absence of expected DOM elements using querySelector() and getAttribute() method.

js
// pagination.spec.js
import React from "react"
import { render } from "@testing-library/react"
import "@testing-library/jest-dom"
import Pagination from "./Pagination"
describe("Pagination", () => {
it("renders first page", () => {
const {container} = render(
<Pagination
isFirst={true}
prevPage={"0"}
isLast={false}
nextPage={"2"}
/>
)
const anchorHref = container.querySelector('div a');
expect(anchorHref.getAttribute("rel")).toEqual("next");
expect(anchorHref.getAttribute("href")).toEqual("/blog/2");
})
it("renders middle page", () => {
const {container} = render(
<Pagination
isFirst={false}
prevPage={"2"}
isLast={false}
nextPage={"4"}
/>
)
const anchorHrefprev = container.querySelector('[rel="prev"]');
const anchorHrefnext = container.querySelector('[rel="next"]');
expect(anchorHrefprev.getAttribute("href")).toEqual("/blog/2");
expect(anchorHrefnext.getAttribute("href")).toEqual("/blog/4");
})
it("renders last page", () => {
const {container} = render(
<Pagination
isFirst={false}
prevPage={"3"}
isLast={true}
nextPage={"5"}
/>
)
const anchorHref = container.querySelector('div a');
expect(anchorHref.getAttribute("rel")).toEqual("prev");
expect(anchorHref.getAttribute("href")).toEqual("/blog/3");
})
})

Seo

This is a slightly more particular component. It receives some props and has dependencies on the useStaticQuery() and useLocation() hook. The first one is used to query with GraphQL siteMetadata (e.g. details in gatsby-config.js) and the last one to access the location object and get the current URL.

These dependencies need to be mocked.

js
// seo.js
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
import { useLocation } from "@reach/router"
const Seo = ({ title, description, image }) => {
const { pathname } = useLocation()
const { site } = useStaticQuery(query)
const {
defaultTitle,
defaultDescription,
siteUrl,
defaultImage,
social,
} = site.siteMetadata
const seo = {
title: title || defaultTitle,
description: description || defaultDescription,
image: image && image.src ? `${siteUrl}${image.src}` : `${siteUrl}${defaultImage}`,
url: `${siteUrl}${pathname}`,
}
return (
<Helmet title={seo.title}>
<html lang="en" />
<meta name="description" content={seo.description} />
<meta name="image" content={seo.image} />
{seo.url && <meta property="og:url" content={seo.url} />}
{seo.title && <meta property="og:title" content={seo.title} />}
{seo.description && (<meta property="og:description" content={seo.description} />)}
{seo.image && <meta property="og:image" content={seo.image} />}
[...]
</Helmet>
)
}
export default Seo
const query = graphql`
query SEO {
site {
siteMetadata {
defaultTitle: title
defaultDescription: description
defaultImage: image
siteUrl
[...]
}
}
}
`

Again the documentation was helpful showing how to do it with the gatsby module.

"Finally, it’s a good idea to mock the gatsby module itself. This may not be needed at first, but will make things a lot easier if you want to test components that use Link or GraphQL. This mocks the graphql() function, Link component, and StaticQuery component."

I added __mocks__ folder and put inside the gatsby module mocked:

js
//gatsby.js
const React = require('react')
const gatsby = jest.requireActual('gatsby')
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
({
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
ref,
replace,
to,
...rest
}) =>
React.createElement("a", {
...rest,
href: to,
})
),
navigate: jest.fn(),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn()
}

Now, I wanted to use the same approach for the @reach/router module. After reading the Jest documentation I found this solution:

"Scoped modules (also known as scoped packages) can be mocked by creating a file in a directory structure that matches the name of the scoped module. For example, to mock a scoped module called @scope/project-name, create a file at mocks/@scope/ project-name.js, creating the @scope/ directory accordingly."

Yup!

So I followed the steps above and inside my __mocks__ folder I added @reach folder and a router.js file, where I mock the useLocation(). Below you can find the content:

js
// router.js
const router = jest.requireActual('@reach/router');
module.exports = {
...router,
useLocation: jest.fn()
}

Now I could simulate the component's behavior using the mockImplementation() function on useStaticQuery() and useLocation() by providing the data from the "About" page and verify that the code works as expected.

js
// seo.spec.js
import React from "react"
import { render } from "@testing-library/react"
import "@testing-library/jest-dom"
import Helmet from "react-helmet"
import SEO from "./Seo"
import { useStaticQuery } from "gatsby" // mocked
import { useLocation } from "@reach/router" // mocked
beforeEach(() => {
useStaticQuery.mockImplementation(() => ({
site: {
siteMetadata: {
defaultTitle: "My Gatsby Site",
defaultDescription: "My Gatsby site used as a blog and portfolio site",
defaultImage: "/images/someimage.jpg",
siteUrl: "http://localhost:8000",
[...]
}
}
})),
useLocation.mockImplementation(() => ({
pathname: "/about/"
}))
})
describe("SEO", () => {
it("renders for about page", () => {
render(<SEO title="About me"/>)
const helmet = Helmet.peek()
expect(helmet.title).toEqual("About me")
expect(helmet.metaTags).toEqual(
expect.arrayContaining([
{ content: "My Gatsby site used as a blog and portfolio site", name: "description" },
{ content: "/images/someimage.jpg", name: "image" },
{ content: "http://localhost:8000/about/", property: "og:url" },
{ content: "About me", property: "og:title" },
{ content: "My Gatsby site used as a blog and portfolio site", property: "og:description" },
{ content: "/images/someimage.jpg", property: "og:image" },
[...]
])
)
})
})

Conclusion

In this post I wanted to tell how I approached the tests with three different examples, each a little more complex than the previous one to show their resolution. In the real world components are certainly more complex but I think it is a good starting point to build more and more powerful tests.

Bye, Alberto