Getting Started with Snapshot Testing Using Jest + Enzyme + GatsbyJS
September 3, 2018 •8 min read
Being new to Jest unit testing and snapshot testing, I find the Jest docs to be too simplistic for real-word application. In this post, I wanted to go through getting started with snapshot testing using Jest in a simple GatsbyJS blog.
There is a lot more to learn than in these simple examples, but this should be helpful in getting your feet wet with a real-world example instead of the simplistic examples found in the docs.
I will be following up this post with other posts on getting started with unit testing and snapshot testing in real-world ReactJS applications as I learn to apply this knowledge myself, so stay tuned!
Install npm packages
Open a terminal window in your project directory. Enter the following command: npm install enzyme enzyme-adapter-react-16 enzyme-to-json jest raf react-addons-shallow-compare react-test-renderer
Or, if using yarn, yarn add enzyme enzyme-adapter-react-16 enzyme-to-json jest raf react-addons-shallow-compare react-test-renderer
This will install the packages we will be using for snapshot testing.
Set up config files
In your src folder, create a folder called tests. In this folder, create a new file setupTests.js. This file will be used to configure Enzyme.
setupTests.js
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
Enzyme.configure({
adapter: new Adapter()
});
In the root of your project, create a new file jest.config.json.
jest.config.json
{
"setupFiles": ["raf/polyfill", "<rootDir>/src/tests/setupTests.js"],
"snapshotSerializers": ["enzyme-to-json/serializer"]
}
In package.json, update the test command in the scripts object to jest --config=jest.config.json
package.json
...
"scripts": {
...
"test": "jest --config=jest.config.json",
...
}
...
Run the test command
Run the test command in the terminal by entering npm run test -- --watch
. The -- --watch
watches for changes in the files and runs the test command automatically.
Create snapshot tests for pages
Create a pages folder in the tests folder if you have a pages folder in src. This is where the tests will be created in separate files for each page. I will start with a 404.js page I have in pages for my custom 404 page.
Create a new file in src/tests/pages called 404.test.js. All tests should be named as file.test.js.
In 404.test.js, import React (since we will be using jsx), shallow from Enzyme, and the exported class for the page. My 404 page exports the class NotFoundPage.
Then create the test by first creating a wrapper using shallow to render the page. We then expect the wrapper to match the snapshot. The first time the test runs, a new snapshot is created and saved in a snapshots folder.
404.test.js
import React from 'react'
import { shallow } from 'enzyme'
import NotFoundPage from '../../pages/404'
test('should render 404 page correctly', () => {
const wrapper = shallow(<NotFoundPage />)
expect(wrapper).toMatchSnapshot()
})
When I created the test file for the blog index page, blog.test.js, I ran into an issue importing the css files for the styles (SyntaxError: Unexpected token .
). To fix this, you will need to use an ES6 proxy and update your jest.config.json. Run npm install --dev identity-obj-proxy
or yarn add --dev identity-obj-proxy
Then, update jest.config.json to handle css files by adding a new moduleNameWrapper object.
jest.config.json
{
"setupFiles": ["raf/polyfill", "<rootDir>/src/tests/setupTests.js"],
"snapshotSerializers": ["enzyme-to-json/serializer"],
"moduleNameMapper": {
"\\.(css|less)$": "identity-obj-proxy"
}
}
The next error I got was ReferenceError: graphql is not defined
. In setupTests.js, add graphql as a jest spy: global.graphql = jest.fn()
setupTests.js
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
Enzyme.configure({
adapter: new Adapter(),
})
global.graphql = jest.fn()
The next issue I ran into was a map method on an array of posts, that should have been fetched from graphql, which was being mocked with the spy, above: TypeError: Cannot read property 'map' of undefined
. To fix this, we will need some default data. Create a folder fixtures in the test folder, and a new file data.js.
I am using Contentful as my graphql data source, so I mocked some data using the structure that Contentful uses.
data.js
export default {
data: {
allContentfulBlog: {
edges: [
{
date: 'August 30, 2018',
slug: 'this-is-a-sample-blog-post',
title: 'This is a sample blog post',
tags: [
{
title: 'Tech',
slug: 'tech',
},
],
},
{
date: 'August 28, 2018',
slug: 'hello-world',
title: 'Hello World!',
tags: [
{
title: 'Tech',
slug: 'tech',
},
{
title: 'Travel',
slug: 'travel',
},
],
},
{
date: 'December 31, 2222',
slug: 'this-is-a-published-post-in-the-future',
title: 'This is a published post in the future',
tags: [
{
title: 'Travel',
slug: 'travel',
},
],
},
],
},
},
}
Jest was also not pulling in the site meta data defined in gatsby-config.js. I modified the data.js to include this data as well.
data.js
export default {
data: {
site: {
siteMetadata: {
title: 'Lipstick, Wine, and Heels',
description:
'Lipstick, Wine, and Heels is a lifestyle blog dedicated to chic living and nerdy topics. I am a woman in tech who appreciates fashion, fine food and drink, crafty hobbies, fitness, and cats.',
siteUrl: 'https://lipstickwineandheels.com',
},
},
allContentfulBlog: {
edges: [
{
date: 'August 30, 2018',
slug: 'this-is-a-sample-blog-post',
title: 'This is a sample blog post',
tags: [
{
title: 'Tech',
slug: 'tech',
},
],
},
{
date: 'August 28, 2018',
slug: 'hello-world',
title: 'Hello World!',
tags: [
{
title: 'Tech',
slug: 'tech',
},
{
title: 'Travel',
slug: 'travel',
},
],
},
{
date: 'December 31, 2222',
slug: 'this-is-a-published-post-in-the-future',
title: 'This is a published post in the future',
tags: [
{
title: 'Travel',
slug: 'travel',
},
],
},
],
},
},
}
I then passed this data in as a prop in my test suite.
blog.test.js
import React from 'react'
import { shallow } from 'enzyme'
import BlogIndex from '../../pages/blog'
import data from '../fixtures/data'
test('should render blog page correctly', () => {
const wrapper = shallow(<BlogIndex data={data.data} />)
expect(wrapper).toMatchSnapshot()
})
I created the remaining pages as test suites. The only new thing I had to do was add additional tag data into data.js.
data.js
export default {
data: {
...
allContentfulTag: {
edges: [
{
title: 'Tech',
slug: 'tech',
},
{
title: 'Travel',
slug: 'travel',
},
],
},
},
}
Create snapshot tests for templates
Create a templates folder in the tests folder if you have a templates folder in src. This is where the tests will be created in separate files for each template. I started with my blog-post.js template by creating a blog-post.test.js file in the templates folder.
The blog post test was not much different from the blog test, but I did need to add data to the data fixture to accommodate a single blog post rather than a list of posts.
blog-post.test.js
import React from 'react'
import { shallow } from 'enzyme'
import BlogPostTemplate from '../../templates/blog-post'
import data from '../fixtures/data'
test('should render blog page correctly', () => {
const wrapper = shallow(<BlogPostTemplate data={data.data} />)
expect(wrapper).toMatchSnapshot()
})
data.js
export default {
data: {
...
contentfulBlog: {
date: 'August 30, 2018',
slug: 'this-is-a-sample-blog-post',
title: 'This is a sample blog post',
postContent: {
childMarkdownRemark: {
html: '<p>This is the post content in html</p>',
},
},
postImage: {
title: 'this is the image title',
description: 'this is the image description',
},
tags: [
{
title: 'Tech',
slug: 'tech',
},
],
},
},
}
For the page template test suite (page.test.js in template folder), I needed to add an additional data object to data.js for the single page data.
data.js
export default {
data: {
...
contentfulPage: {
title: 'About',
slug: 'slug',
pageContent: {
childMarkdownRemark: {
html: '<p>Learn all about me...</p>',
},
},
metaDescription: 'This page is all about me',
pageImage: {
sizes: {
src: '/reference/to/some/image.jpg',
},
},
},
},
}
Create snapshot tests for components
Create a components folder in the tests folder if you have a components folder in src. This is where the tests will be created in separate files for each component. I started with my article-preview.js component by creating an article-preview.test.js file in the components folder.
The article preview component expects props to be passed in, so I used data from my data fixture but passed it in a little differently by pulling the individual blog post and passing it in, instead of passing all the data.
article-preview.test.js
import React from 'react'
import { shallow } from 'enzyme'
import ArticlePreview from '../../components/article-preview'
import data from '../fixtures/data'
test('should render article preview component correctly', () => {
const wrapper = shallow(<ArticlePreview article={data.data.contentfulBlog} />)
expect(wrapper).toMatchSnapshot()
})
For components like the footer and navigation that did not expect any props, I simply removed the data import and props.
navigation.test.js
import React from 'react'
import { shallow } from 'enzyme'
import Navigation from '../../components/navigation'
test('should render navigation component correctly', () => {
const wrapper = shallow(<Navigation />)
expect(wrapper).toMatchSnapshot()
})
Setting up Jest code coverage
Jest comes with code coverage that allows you to see what you've written tests for and what still needs to be written. We just need to do a little bit of configuring.
In your package.json, add another script called test-coverage with the command jest --config=jest.config.json --coverage --collectCoverageFrom=src/**/*.js
package.json
...
"scripts": {
"test-coverage":
"jest --config=jest.config.json --coverage --collectCoverageFrom=src/**/*.js",
...
}
To check test coverage, use npm run test-coverage
in the terminal. Jest will output a table of the results, as well as an html version in a coverage/lcov-report folder at the root of the project. If you are using git, you will want to ignore the coverage folder.
Conclusion
That's it for now. Feel free to comment below if anything doesn't make sense or if you have suggestions of your own!
Share
Think others might enjoy this post? Share it!
Comments
I'd love to hear from you, let me know your thoughts!