Joshua's Cheatsheets - JavaScript Testing - Notes, Options, etc.
Light
help

Resources

Common Test Patterns

Where to put tests?

  • The main options are to either:

    • Store in completely separate directory, e.g. /__tests__
    • Store alongside source files - e.g. src/main.js and src/main.tests.js

Common Subdirectories or Companions

There are some common subdirectories and/or types of test supports:

  • fixtures

    • This should contain static data, that can be used to test with
  • snapshots

    • Snapshots are usually files that are not hand-coded; they are generated based on your code, and usually represent a section of output

      • The idea is to not re-generate them every time; you generate them when your app is in a good state
      • A snapshot test generates a new snapshot whenever it runs, and compares it with the stored snapshot; if they don't match, that is an instant fail
    • A snapshot doesn't have to about UI elements; it just needs to be of any serialisable value.
    • The usefulness of these are often debated - see Effective Snapshot Testing, by Kent C. Dodds

      • One major caveat is that you need to be very sure that the stored snapshot is a valid snapshot; anything else is worse than having no snapshot at all.
  • helpers

    • Contains methods / classes / utilities that help with your tests
    • Should probably not contain any actual data (leave that in fixtures)
  • mocks

    • Generic, vague definition: A mock is a fake version of a real "thing", that emulates the real behavior, but otherwise is an incomplete (and optimally much smaller) version of the actual thing.

      • Useful for testing because often using the full version of everything can slow down tests
      • Mocking is a broad topic in testing. Also related to stub, spy, and dummy

    • Jest has some built-in support for manual mocking, but for everything else or with other testers, you usually want something like sinon.js

Mocks vs Stubs

General summary:

  • A stub is usually extremely simple (a "stub" of a thing!) - essentially just a tiny bit of code that says "given x input always return y" - they are not dynamic, and the expected input and output is known statically. In comparison, a mock, although fake and incomplete, should still have an interface that mirrors the real thing; this distinction means that a mock ensures methods calls are followed correctly.

Since I had some trouble making the distinction in my brain click, I'm going to write out a few different ways to summarize the distinction outlined above:

  • One of my favorites: "Stubs don't fail your tests, mock can" (S/O)
  • Another great distinction: Mocks are testing behavior, stubs are testing state
  • Another way to think: Essentially all mocks are stubs - or, an arrangement of stubs

If you are still feeling stuck, there are lots of great responses to both of these Stack Overflow questions:

Jest

File Structure / Glob Patterns

Main doc: config / testRegex

  • Default pattern is (/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$

Default Config

  • Directories:

    • /__tests__
  • Files (where {ext} is (.js | .jsx | .ts | .tsx))

    • .test.{ext}
    • .spec.{ext}
    • test.{ext}
    • spec.{ext}

Examples

TypeScript Injected Globals

Save-dev @types/jest.

If using in plain JS with @ts-check, you can use triple slash ->

// @ts-check
/// <reference types="jest" />
describe('My Test', () => {
	//
});

JSDOM with Jest

By default, Jest ships with and uses JSDOM as the default test environment. It also exposes associated variables as globals, which means you can use things like document.querySelector() inside Jest tests with no extra setup required.

Also see: Jest - DOM Manipulation

Access to JSDOM config, settings, etc.

Unfortunately, Jest seems to mostly just expose the DOM from JSDOM (via document global), and not the controls of JSDOM itself. For example, by default you can't access the JSDOM config, or use JSDOM methods directly.

There are some workarounds though:

  • Pass options to JSDOM through testEnvironmentOptions

    • If all you need to do is modify JSDOM defaults, and don't need access to JSDOM itself, this should work fine
  • Install jsdom as a dependency and just use normally
  • Use jest-environment-jsdom (ships with Jest) to access the config

    • See directions here
  • Install a package that automatically does the above step, of exposing Jest via jest-environment-jsdom - simon360/jest-environment-jsdom-global

Ava

Grouping Tests and Multiple Test Files

The recommended way (unless things change) to group tests (or emulate test suites) is by splitting up your tests by file.

There are two options for using multiple test files with Ava:

  • Option A: Make sure your files match the glob pattern matching of Ava

    • Ava looks for certain patterns of files to automatically detect as tests. Glob patterns are here, and documented here.
    • Directories (grabs all files inside):

      • /__tests__
      • /test
      • /tests
    • File patterns:

      • *.spec.{ext}
      • *.test.{ext}
      • *test-*.{ext}
      • test.{ext} (this is the default starter file)
  • Option B: Pass specific files via the CLI

Mocha

How to run a specific test

mocha --grep "{describeTextPattern}"

How to run a single test file / specific file

Just pass as last argument / input to mocha CLI. In the docs, they call this spec.

If you want to add a dedicated command to your package.json, so devs can run with a bunch of hard-coded flags before the filename, you might run into issues if you have hardcoded the glob pattern into mocha.opts or a different config file (see wont-fix GH issue).

If the above is true for you, I would recommend removing the hardcoded pattern from your options file and rewrite scripts to look something like this:

{
	"scripts": {
		"test": "mocha {globPattern}",
		"test-file": "mocha"
	}
}

Code coverage

Most popular is probably istanbul; it's built into Jest, and comes recommended with Ava.

Ignoring stuff


Browser / DOM Testing

Running browser-based code

First, it is important to note that there are different types of testing methods when it comes to testing code that manipulates or generates DOM / HTML / browser code, and/or uses standard browser APIs.

In general, these can be divided into two categories:

  • Mocking large parts of the browser DOM logic in JS - this is pretty much just JSDOM

    • Essentially the engine / DOM processing part of the browser is mocked entirely in JS, but not any of the actual rendering / UI / etc.
    • Tons of testing libraries use this as the actual environment to run your test in.

      • Examples: Jest, Enzyme, Testing-Library (via Jest), and more
    • There are also companion libs for working with the global window object / augmenting

    • Advantage: It is very fast compared with actually running a full browser
    • Disadvantage: It is not a true browser test, and can't be used for tests that verify UI, rendering, layout, etc.

      • There are also some huge holes in what standard web APIs are emulated in JSDOM. For example...

        • HTMLElement.innerText is still not supported in 2020, despite having about 99% of real-world browser support.
        • window.getSelection()
        • etc.
  • Automated running of an actual browser process

    • Basically just an automated runner that hooks into a real browser process
    • Popular examples are playwright, puppeteer, Cypress, and TestCafe.
    • Advantages:

      • Apart from manual testing by hand, this is basically the closest you can get to a real 1:1 test of your app that puts it through the same environment that your users use.

        • This means you can test against browser quirks, different rendering engines, etc.
      • Most of these automated browser testers support a "headless" mode, which basically runs the browser without bothering to actually render the pixels on the screen (but still capturing the output).

        • Although this doesn't give enough of a speed boost to match something like JSDOM, this is still a considerable performance boost over non-headless
      • Some testers support multiple browsers with the same API - Microsoft Playwright is a somewhat new entrant that does this and looks to be extremely promising.
      • Can be used for more than just testing!!!

        • For example, a common alternative use-case is generating screenshots or PDF captures of generated webpages.
    • Disadvantages

      • Slow(er) and heavier: Running a real browser, headless or not, requires more resources than adding a some extra NodeJS code to an existing NodeJS app.

        • Requires more resources (CPU, RAM, and storage) for wherever your tests are running (local, Jenkins, etc.)
      • More complex: There are extra layers of abstraction (JS, APIs, OS details, etc), different Operating Systems and their peculiarities, and many more details that have to work together to make this work.

Another way to look at the types above is that JSDOM is usually good enough for unit, or integration tests, where all you need is a quick diff between expected HTML and actual HTML, but for an "end-to-end" (E2E), or functional test that is for something that runs in a browser, and needs to test behavior or appearance, you probably need a true browser runner.

Reading in HTML or JSX Files

If you are building code that manipulates the DOM and looking to test that functionality, you might be wondering how to actually feed elements into the tester.

Vanilla HTML

If you are trying to load vanilla HTML into the test, you have several options:

  • Composing an HTML string manually

    • Regular String: document.body.innerHtml = '<p>' + myText + '<p>';
    • Template Literal: document.body.innerHtml = `<p>${myText}</p>`;
    • If you are using JSDom, you might want to pass it in the constructor:

      • const dom = new jsdom('<p>' + myText + '<p>')
  • Feeding in a saved HTML file with fs

    • document.body.innerHtml = fs.readFileSync('./fixtures/test-page.html', 'utf8')
  • Programmatically with DOM APIs

    • document.createElement() etc.
  • Via explicit loader functions (JSDom)

    • const dom = await JSDOM.fromFile('./fixtures/test-page.html')

This is a good summary for JSDom: https://dustinpfister.github.io/2018/01/11/nodejs-jsdom/

JSX

Since so many testing libraries and utilities are focused on React at the moment (understatement), the nice thing is that pulling in and testing JSX is baked into most libraries and made to be as easy as possible. Usually, the setup for pulling in JSX is just two steps:

  1. Import the actual component, just like you normally would in React

    • E.g. import MyComponent from '../src/components/MyComponent' (.jsx file)
  2. Load the JSX into the DOM, or into a snapshot (examples of methods below)

Loading JSX into DOM or Snapshot

Examples / basic API with different test libs:

Enzyme

Shallow Render:

import React from 'react';
import MyComponent from '../src/components/MyComponent';
import { shallow } from 'enzyme';
// ...
const wrapper = shallow(<MyComponent myProp={val} />);

Full Render:

import React from 'react';
import MyComponent from '../src/components/MyComponent';
import { mount } from 'enzyme';
// ...
const wrapper = mount((
	<MyComponent myProp={val} />
));
React-Testing-Library

React-Testing-Library render:

import React from 'react';
import MyComponent from '../src/components/MyComponent';
import { render, fireEvent, waitForElement } from '@testing-library/react'
// ...
const testUtils = render(<MyComponent myProp={val} />);
Jest

For a snapshot:

import React from 'react';
import MyComponent from '../src/components/MyComponent';
import renderer from 'react-test-renderer';
// ...
const component = renderer.create(<MyComponent myProp={val} />);

For DOM testing:

  • They actually recommend that you use react-testing-library, or Enzyme
  • For a TestUtils approach, see below ("React Built-Ins")

React Built-Ins

There are actually some built-in JSX test utilities that you probably already have access to if you are using React. They are in the react-dom package, which you should have as a default dependency if you used create-react-app.

There are a bunch of helpful methods, but the most helpful for loading JSX is renderIntoDocument():

import React from 'react';
import MyComponent from '../src/components/MyComponent';
import ReactTestUtils from 'react-dom/test-utils';
// ...
const component = ReactTestUtils.renderIntoDocument(<MyComponent myProp={val} />);
  • Etc...

Additional dependencies

If you are starting with create-react-app, or a plug-n-play test solution, dependencies should be pretty straightforward. Regardless, usually there are two main dependencies required:

  • React

    • Required, since JSX really just transpiles to calls to React.createElement()
  • A library method to load the React element into either JSDOM or a snapshot

    • The Jest docs has a pretty good overview of how this works across different test tools
    • Jest uses react-test-renderer for snapshots, but for loading into JSDOM, there are multiple options (see above section on loading JSX into DOM)

Depending on your needs, you might also need Babel to transpile code.

Executing JavaScript with JSDOM

Script Execution Settings

First, if you are looking for how to let scripts (inline or external) load and execute on the initial DOM load, take a look at the "executing scripts" part of the main readme. Basically, you need to tweak the default settings:

{
	"testEnvironmentOptions": {
		"runScripts": "dangerously",
		"resources": "usable"
	}
}

How to add or eval scripts on the fly

Once you already have a DOM loaded, how do you programmatically add / execute new scripts?

There are a few ways to execute JS with JSDOM, and not all are equal.

Via script tag

Just like in a real browser, an option for executing scripts is to actually inject new <script> tags into the DOM. For example, take this sample Jest test:

test('Injects script via tag', () => {
	const scriptElem = document.createElement('script');
	const scriptText = document.createTextNode(`window.testString = 'abc123';`);
	scriptElem.appendChild(scriptText);
	document.body.appendChild(scriptElem);
	// window = global here
	expect(global.testString).toEqual('abc123');
});

Warning: Although this works fine in browsers, setting the text of the tag via scriptElem.innerText = "" does not work in JSDom for some reason; it ends up injecting an empty script tag.

Via eval()

You can also execute code via window.eval(), again similar to a native browser. In both the script tag method and eval, the executed code has access to the DOM managed by JSDOM as well. For example:

test('Injects via eval', () => {
	global.eval(`document.querySelectorAll("pre").forEach(elem => elem.remove());`);
	const preCount = document.querySelectorAll("pre").length;
	expect(preCount).toEqual(0);
});
Markdown Source Last Updated:
Sun May 24 2020 07:44:32 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Tue Sep 17 2019 10:21:28 GMT+0000 (Coordinated Universal Time)
© 2020 Joshua Tzucker, Built with Gatsby