Testing
Creating robust Gatsby web applications using Typescript, Jest, and testing-library
Gatsbyjs is a Jamstack implementation that can support UI testing using Jest and testing-library.
Setup
Install
npm install --save-dev jest babel-jest babel-preset-gatsby identity-obj-proxy
npm install --save-dev @testing-library/react @testing-library/jest-dom
npm install --save-dev @types/jest @babel/preset-typescript
Jest28+ also requiresnpm install --save-dev jest-environment-jsdom
Config
Basic testing setup is a little tedious, but not hard.
- setup the test environment
- setup
__mocks__
- setup package.json
- snapshot testing
- complex setup
test environment
These files are in the root of the project directory.
- jest-preprocess.js
- jest.config.js - this was updated heavily in jest28
- setup-test-env.js
- loadershim.js
jest-preprocess.js
const babelOptions = {
presets: ["babel-preset-gatsby", "@babel/preset-typescript"],
}
module.exports = require("babel-jest").default.createTransformer(babelOptions)
jest.config.js - this was updated heavily in jest28
- jest27 requires addition of testEnvironment:
jsdom
module.exports = {
transform: {
"^.+\\.[jt]sx?$": "<rootDir>/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>/__mocks__/file-mock.js`,
"^gatsby-plugin-utils/(.*)$": [
`gatsby-plugin-utils/dist/$1`,
`gatsby-plugin-utils/$1`,
], // Workaround for https://github.com/facebook/jest/issues/9771
},
testPathIgnorePatterns: [`node_modules`, `\\.cache`, `<rootDir>.*/public`],
transformIgnorePatterns: [`node_modules/(?!(gatsby|gatsby-script|gatsby-link|uuid)/)`],
globals: {
__PATH_PREFIX__: ``,
},
setupFiles: [`<rootDir>/loadershim.js`],
testEnvironment: `jsdom`,
setupFilesAfterEnv: ["<rootDir>/setup-test-env.js"],
testTimeout: 30000
}
setup-test-env.js
import "@testing-library/jest-dom/extend-expect"
// mock this here for early media requests (dark mode)
window.matchMedia = (query) => ({
matches: false,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})
jest.mock("gatsby-plugin-image", () => {
const React = require("react")
const plugin = jest.requireActual("gatsby-plugin-image")
const mockImage = ({imgClassName, ...props}) =>
React.createElement("img", {
...props,
className: imgClassName,
})
const mockPlugin = {
...plugin,
GatsbyImage: jest.fn().mockImplementation(mockImage),
StaticImage: jest.fn().mockImplementation(mockImage),
}
return mockPlugin
})
loadershim.js
global.___loader = {
enqueue: jest.fn(),
}
setup mocks
- aws-amplify.js - mocks for Auth, Storage, Hub, etc
- file-mock.js -
module.exports = "test-file-stub"
- gatsby.js - mocks for gatsby functions
- navigate, graphql, Link, StaticQuery, useStaticQuery
Optionally add things like this example,
which mocks up the Authenticator component (see WD amplify-ui for more
information on amplify UI components)
__mocks__/@aws-amplify/ui-react.js
import React from 'react';
export const Authenticator = props => {
return (<div data-testid='authenticator'>{props.children}</div>);
}
export const useAuthenticator = props => {
const user = {
username: 'testusr',
}
return ({ user });
}
Typescript
Setup
tsconfig.json
// basic gatsbyjs tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"lib": ["dom", "esnext"],
"moduleResolution": "node",
"jsx": "preserve",
"strict": true,
"noEmit": true,
"isolatedModules": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"skipLibCheck": true,
"strictBindCallApply": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noFallthroughCasesInSwitch": true,
},
"exclude": ["node_modules/*", "public/*", ".cache/*", "coverage/*", "amplify/*"]
}
Snapshots
Example (using testing-library and asFragment)
const mytest = <ModifyEvent evid='_new' tasks={mockEvents} onComplete={mockCallback} open={true} />;
describe("eventsutil - create", () => {
it("renders snapshot correctly", () => {
const {asFragment} = render(mytest);
expect(asFragment()).toMatchSnapshot();
});
}
Using asFragment also allows you to check for blank snapshots.
git grep "<DocumentFragment />" *.snap
Using testing-library and container
Most of the snapshot examples (including the Gatsby example) uses the container object. This is sub-optimal because:
- react fragments render as null without warning
- hard to find other null snapshots
- extra line splits and other readability issues
const mytest = <ModifyEvent evid='_new' tasks={mockEvents} onComplete={mockCallback} open={true} />;
describe("eventsutil - create", () => {
it("renders snapshot correctly", () => {
const {container} = render(mytest);
expect(container.firstChild).toMatchSnapshot();
});
});
Using react-test-renderer
You can also use the jest react-test-renderer, but it has extra information that clutters up the snapshot. The create() function tests the react shadow DOM. Use render to test against the DOM.
const snap = renderer.create(mytest).toJSON();
expect(snap).toMatchSnapshot();
Boilerplate
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from '@testing-library/user-event'
const mytest = <ModifyEvent evid='_new' tasks={mockEvents} onComplete={mockCallback} open={true} />;
const mySetup = () => {
const utils = render(mytest);
const resetButton = utils.getByRole('button', {name: /reset/i});
const saveButton = utils.getByRole('button', {name: /save/i});
const newRuleButton = utils.getByRole('button', {name: /new rule/i});
const nameFld = utils.getByTestId('nameInput');
const descrFld = utils.getByTestId('descrInput');
return {
...utils,
resetButton,
saveButton,
newRuleButton,
nameFld,
descrFld,
}
};
describe("eventsutil - create", () => {
it("renders snapshot correctly", () => {
const {asFragment} = render(mytest);
expect(asFragment()).toMatchSnapshot();
});
it("handles graphql error on save", async () => {
const consoleWarnFn = jest.spyOn(console, 'warn').mockImplementation(() => jest.fn());
const prevAPIgraphql = API.graphql;
API.graphql = jest.fn(() => Promise.reject('mockreject')) as any;
const utils = mySetup();
await userEvent.type(utils.nameFld, 'newgrp');
await userEvent.type(utils.descrFld, 'new desc');
await waitFor(() => {
expect(utils.resetButton).toBeEnabled();
});
expect(utils.saveButton).toBeEnabled();
userEvent.click(utils.saveButton);
await waitFor(() => {
expect(consoleWarnFn).toHaveBeenCalledTimes(1);
});
API.graphql = prevAPIgraphql;
consoleWarnFn.mockRestore();
});
});
Notable upgrades
- Jest v29.x (not much change)
- testing-library v13
- Jest v28.x
testing-library v13
also includes testing-library/jest-dom
installs
npm i --save-dev @testing-library/react@13 @testing-library/user-event@14
(optional)
npm i --save-dev @testing-library/dom@8 @testing-library/jest-dom@5
Jest v29.x
npm i --save-dev jest@29 jest-environment-jsdom@29 babel-jest@29 @types/jest@29
Typescript
import {expect, jest, test} from '@jest/globals';
Snapshot Differences
Jest v28.x
npm i --save-dev jest@28 jest-environment-jsdom@28 babel-jest@28 @types/jest@28
Config issues
jest.config.js - this was updated heavily in jest28 see setup test environment for current example