(Edit October 10, 2018) Note: This article is now out of date but is kept published for reference purposes. Nearly everything in Gutenberg is published as a package so for the purpose of testing you can include those packages as a devDependency
in your package.json
file and jest will know to reference those in tests.
As a part of my work with Event Espresso I’ve been assisting them with moving over to a more modern Javascript build system. This has been prompted by the upcoming new editor for WordPress codenamed Gutenberg. I’m not going to spend a whole lot of time talking about Gutenberg since there’s already a ton of information on the internet. What I want to do in this post is outline a way for plugin developers to test their custom components/blocks built for Gutenberg using the Jest framework.
Writing javascript tests using Jest is pretty straightforward and this isn’t a how-to-post since there’s a lot of that already available (including this readme the Gutenberg team prepared). Instead I thought it’d be useful to write how I solved a problem with a particular set of tests I was trying to write for some code I had written (especially since I couldn’t find anything useful on the internet myself to help with it).
The Problem
For Event Espresso I’m creating a reusable react component that exposes a Event Selector populated with events currently on the user’s site. This will end up getting used in various parts of the Gutenberg friendly api we’re building. I wanted a way to test this component using jest but ran into a few problems:
- The
EventSelect
component is composed of thePlaceholder
,SelectControl
, andSpinner
components from@wordpress/components
(orwp.elements
). - There’s a few other dependencies in file for this component including
@wordpress/i18n
and@wordpress/data
- Gutenberg itself is a plugin and most of the dependencies the code has on Gutenberg itself are thus not available in the context of the javascript being tested (because of assumption of Gutenberg functionality being exposed on the
wp
global at runtime). - I want to at a minimum use jest’s snapshot feature (using
shallow
) to verify thatEventSelect
is rendered correctly depending on the various props provided to it.
While I could mock all of the dependencies, mocking the WordPress components being used would mean needing to make the assumption they would never change.
The Solution
I’ll get into the specifics of how I fixed this first, and then I’ll take some time to explain why it works (or in some cases why I think it works because frankly I’m still learning Jest and its intricacies).
The very first thing that needs done is importing gutenberg as a package. Wait what? Gutenberg is an npm package? It’s not published to npm, however technically, anything with a npm package.json
file in it is already a package. If it’s hosted on github, you can install it. So the first step to exposing gutenberg javascript for our tests is to install it! How do you do that?
npm install WordPress/gutenberg#master --save-dev
That’s it! In case you’re wondering, npm automatically looks on github for packages if they aren’t in the npm package repository. So the format I used corresponds to the github {organization}/{repo}#{branch}
format.
Next there’s some configuration file changes needed.
The jest.config.json
file ended up with this.
{
"rootDir": "../../../",
"collectCoverageFrom": [
"assets/src/**/*.js",
"!**/node_modules/**",
"!**/vendor/**",
"!**/test/**"
],
"moduleDirectories": ["node_modules"],
"moduleNameMapper": {
"@eventespresso\\/(eejs)": "assets/src/$1",
"@wordpress\\/(blocks|components|date|editor|element|data|utils|edit-post|viewport|plugins|core-data)": "<rootDir>/node_modules/gutenberg/$1",
"tinymce": "<rootDir>/tests/javascript-config/unit/mocks/tinymce",
"@wordpress/i18n": "<rootDir>/tests/javascript-config/unit/mocks/i18n"
},
"setupFiles": [
"core-js/fn/symbol/async-iterator",
"<rootDir>/tests/javascript-config/unit/setup-globals"
],
"preset": "@wordpress/jest-preset-default",
"testPathIgnorePatterns": [
"/node_modules/",
"/test/e2e"
],
"transformIgnorePatterns": [
"node_modules/(?!(gutenberg)/)"
]
}
Three particular properties you should take note of are:
moduleNameMapper
The property allows you to map module names to a specific location or to a mock. In this case I wanted to map all @wordpress/*
packages to the gutenberg package I just installed. So this line does exactly that:
"@wordpress\\/(blocks|components|date|editor|element|data|utils|edit-post|viewport|plugins|core-data)": "<rootDir>/node_modules/gutenberg/$1",
Wait a minute! How can you be using things like@wordpress/data
for your imports if that isn’t a package?
Most Gutenberg tutorials you see on the internet give instructions for using the wp
global as an external in your js source files, resulting in something like this:
const { Placeholder } = wp.components;
const { withSelect } = wp.data;
This works just fine, however, if Gutenberg ever publishes some of these things as a bona-fide package, and you want to use that package, you’d have to go through and change every file in your source to to imports. I’m lazy, I don’t want to have to go through all my files if that happens. So instead I want to do this:
import { Placeholder } from '@wordpress/components';
import { withSelect } from '@wordpress/data';
However, if I do that, Webpack won’t know what to do with those imports because @wordpress/components
and @wordpress/data
don’t exist in node_modules
. So we need to help webpack out. In the webpack configuration file I have something like this:
const externals = {
'@wordpress/data': {
this: [ 'wp', 'data' ],
},
'@wordpress/components': {
this: [ 'wp', 'components' ],
},
};
The externals
object is then assigned to the externals
configuration property for webpack. What this does is alias@wordpress/data
to the wp.data
global. So on builds any imports are correctly translated to use the wp.data
global.
However, when running jest tests, jest does not work with the webpack builds (obviously) and is oblivious to this aliasing. So when it encounters the imports (and goes to translate them to commonjs modules) it can’t find the @wordpress/*
packages. We get around that by using the moduleNameMapper
property.
You’ll also notice that I mapped tinymce
to a specific location for a mock. What I discovered is that jest will follow the package import chain all the way down. I would hit import not found
for tinymce because guess what? Gutenberg itself sets up tinymce as an external in its webpack config. Since I’ll never need tinymce for my own testing (and shouldn’t need it anyways), I can just mock it. So this line ends up being:
"tinymce": "<rootDir>/tests/javascript-config/unit/mocks/tinymce",
The path it points to simply contains this the tinymce.js
file I created with this as its contents:
module.exports = jest.fn()
Which is just a handy method jest provides for mocking.
Finally, you’ll see that I also mock @wordpress/i18n
. The interesting thing here is that because @wordpress/i18n
exists as a package. I didn’t have to mock it. However, I don’t really need to initialize jed or anything with i18n so to avoid errors related to it not being initialized in the test environment (and the usage of an actual text_domain in any i18n function calls), its better to just mock it. So this line…
"@wordpress/i18n": "<rootDir>/tests/javascript-config/unit/mocks/i18n"
…points to the i18n.js
file which currently has this as its contents:
module.exports = {
__ : (string) => {
return string;
}
};
Right now I’m only mocking the __
function, but as I start testing various code that utilizes other wp.i18n
functions I’ll add them to this as well.
The next property I want to zero in on in the jest.config.json
file is the setupFiles
property. This property is used to indicate any files you want executed before jest runs tests. In particular, take note of this file reference:
"<rootDir>/tests/javascript-config/unit/setup-globals"
This points to a setup-globals.js
file at that path and its contents are this:
/**
* Setup globals used in various tests
*/
// Setup eejsdata global. This is something set in EE core via
// wp_localize_script so its outside of the build process.
global.eejsdata = {
data: {
testData: true
},
};
// Set up `wp.*` aliases. Doing this because any tests importing wp stuff will
// likely run into this.
global.wp = {
shortcode: {
next() {},
regexp: jest.fn().mockReturnValue( new RegExp() ),
},
};
[
'element',
'components',
'utils',
'blocks',
'date',
'editor',
'data',
'core-data',
'edit-post',
'viewport',
'plugins',
].forEach( entryPointName => {
Object.defineProperty( global.wp, entryPointName, {
get: () => require( 'gutenberg/' + entryPointName ),
} );
} );
This is needed not because I’m going to be using the wp
global anywhere in the source js I’m testing, but because I’m actually importing Gutenberg source for the tests, some of the Gutenberg files ARE referencing the wp globals and thus they need to be defined to avoid undefined
errors when running tests. With this bit of code, we’re linking up the wp.{module name}
with its module location in the node_modules
folder. So for instance anytime jest encounters wp.blocks
it will set the property to the module loaded from node_modules/gutenberg/blocks
.
The final property I want to zero in on for the jest.config.json
file is the transformIgnorePatterns
property. For a while I was struggling with the following error every time I ran a test (prior to this property value being inserted):

Jest expects packages that are included to be pre-compiled (i.e from ES6 to commonjs
). So if you have any imports that point to uncompiled packages you’ll get the above kind of error. Since we’re including gutenberg
as a package straight from its repo, we don’t have the compiled assets, hence the need to let jest know it needs to transform any imports it comes across in gutenberg
. I accidentally discovered this when I was reading through the Jest documentation and came across this line:
Since all files inside
Jest Documentation fornode_modules
are not transformed by default, Jest will not understand the code in these modules, resulting in syntax errors.transformIgnorePatterns
Oooooo, ya I’m getting syntax errors. What I didn’t realize is that this property allows you to whitelist things you want to be transformed. Groovy, so as a reminder here’s what I ended up using:
"transformIgnorePatterns": [
"node_modules/(?!(gutenberg)/)"
]
This then made sure that anytime the node_modules/gutenberg
package (or any dependencies in those packages) were imported, the code referenced would be transformed into something jest can work with. Say good bye to syntax errors!
Winner winner, chicken dinner!
With all that work, the code I wanted tested:
/**
* External dependencies
*/
import { stringify } from 'querystringify';
import moment from 'moment';
import { isUndefined, pickBy, isEmpty } from 'lodash';
import PropTypes from 'prop-types';
/**
* WP dependencies
*/
import { Placeholder, SelectControl, Spinner } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { withSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { buildEventOptions } from './build-event-options';
const nowDateAndTime = moment();
export const EventSelect = ( {
events,
onEventSelect,
selectLabel,
selectedEventId,
isLoading,
} ) => {
if ( isLoading || isEmpty( events ) ) {
return <Placeholder key="placeholder"
icon="calendar"
label={ __( 'EventSelect', 'event_espresso' ) }
>
{ isLoading ?
<Spinner /> :
__(
'There are no events to select from. You need to create an event first.',
'event_espresso'
)
}
</Placeholder>;
}
return <SelectControl
label={ selectLabel }
value={ selectedEventId }
options={ buildEventOptions( events ) }
onChange={ ( value ) => onEventSelect( value ) }
/>;
}
/**
* @todo some of these proptypes are likely reusable in various place so we may
* want to consider extracting them into a separate file/object that can be
* included as needed.
* @type {{events: *, onEventSelect, selectLabel: *, selectedEventId: *,
* isLoading: *, attributes: {limit: *, orderBy: *, order: *, showExpired: *,
* categorySlug: *, month: *}}}
*/
EventSelect.propTypes = {
events: PropTypes.arrayOf( PropTypes.shape( {
EVT_name: PropTypes.string.required,
EVT_ID: PropTypes.number.required,
} ) ),
onEventSelect: PropTypes.func,
selectLabel: PropTypes.string,
selectedEventId: PropTypes.number,
isLoading: PropTypes.bool,
attributes: PropTypes.shape( {
limit: PropTypes.number,
orderBy: PropTypes.oneOf( [
'EVT_name',
'EVT_ID',
'start_date',
'end_date',
'ticket_start',
'ticket_end',
] ),
order: PropTypes.oneOf( [ 'asc', 'desc' ] ),
showExpired: PropTypes.bool,
categorySlug: PropTypes.string,
month: PropTypes.month,
} ),
};
EventSelect.defaultProps = {
attributes: {
limit: 20,
orderBy: 'start_date',
order: 'desc',
showExpired: false,
},
selectLabel: __( 'Select Event', 'event_espresso' ),
isLoading: true,
events: [],
};
/**
* Used to map an orderBy string to the actual value used in a REST query from
* the context of an event.
* @todo this should be moved to a mapper library for various EE Rest Related
* things maybe?
*
* @param {string} orderBy
*
* @return { string } Returns an actual orderBy string for the REST query for
* the provided alias
*/
const mapOrderBy = ( orderBy ) => {
const orderByMap = {
start_date: 'Datetime.DTT_EVT_start',
end_date: 'Datetime.DTT_EVT_end',
ticket_start: 'Datetime.Ticket.TKT_start_date',
ticket_end: 'Datetime.Ticket.TKT_end_date',
};
return isUndefined( orderByMap[ orderBy ] ) ? orderBy : orderByMap[ orderBy ];
};
const whereConditions = ( { showExpired, categorySlug, month } ) => {
const where = [];
const GREATER_AND_EQUAL = encodeURIComponent( '>=' );
const LESS_AND_EQUAL = encodeURIComponent( '<=' );
if ( ! showExpired ) {
where.push( 'where[Datetime.DTT_EVT_end**expired][]=>&where[Datetime.DTT_EVT_end**expired][]=' +
nowDateAndTime.local().format() );
}
if ( categorySlug ) {
where.push( 'where[Term_Relationship.Term_Taxonomy.Term.slug]=' + categorySlug );
}
if ( month && month !== 'none' ) {
where.push( 'where[Datetime.DTT_EVT_start][]=' + GREATER_AND_EQUAL + '&where[Datetime.DTT_EVT_start][]=' +
moment().month( month ).startOf( 'month' ).local().format() );
where.push( 'where[Datetime.DTT_EVT_end][]=' + LESS_AND_EQUAL + '&where[Datetime.DTT_EVT_end][]=' +
moment().month( month ).endOf( 'month' ).local().format() );
}
return where.join( '&' );
};
export default withSelect( ( select, ownProps ) => {
const { limit, order, orderBy } = ownProps.attributes;
const where = whereConditions( ownProps.attributes );
const { getEvents, isRequestingEvents } = select( 'eventespresso/lists' );
const queryArgs = {
limit,
order,
order_by: mapOrderBy( orderBy ),
};
let queryString = stringify( pickBy( queryArgs,
value => ! isUndefined( value ),
) );
if ( where ) {
queryString += '&' + where;
}
return {
events: getEvents( queryString ),
isLoading: isRequestingEvents( queryString ),
};
} )( EventSelect );
And the actual test I tried to get working:
import { shallow } from 'enzyme';
import { EventSelect } from '../index';
describe( 'EventSelect', () => {
it( 'should render', () => {
const wrapper = shallow( <EventSelect /> );
expect( wrapper ).toMatchSnapshot();
} );
} );
results in a winner!

I’m not done yet, but the big part is done, now I can go ahead and write tests for various states of the component.
Any thoughts? I’m pretty new to jest so there may be some things I’m not doing “right”. If you’re a javascript/jest guru and have some pointers to help improve on this I’m all ears 🙂
If you want to follow along with my journey in writing this component and the related tests etc, you can go to this pull request.