Werner Digital

Technology

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.

Content

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 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

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

Timing Differences