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-typescriptJest28+ also requires
- npm 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
