Jest Snapshot testing within Rails Webpacker
Ah… The state of the Rails ecosystem couldn’t be better for me. With the supplementing of the asset pipeline with Webpacker the world of Javascript development has joined nicely with the world of Rails development.
If you are like me and using ReactJS as your staple JS view layer, you probably know of the testing tool Jest also created by the smart people at Facebook. This short tutorial assumes you have Webpacker and ReactJS running and you want to get setup with Jest.
Setup
Install your packages
yarn
or npm
install the following packages
jest
(the main testing library)babel-jest
(assuming you are usingbabel
, this is the associatedjest
plugin)babel-preset-es2015
(Babel preset for all es2015 plugins)babel-preset-react
(Babel preset for react)react-test-renderer
(another FB testing tool which renders React components to pure Javascript objects)
yarn
does a better job of managing your JS packages, so here you go…
yarn add --dev jest babel-jest babel-preset-es2015 babel-preset-react react-test-renderer
Add the .baberc
file to your project root
Till this point, your project has been happily using the config/webpack/loaders/react.js
and config/webpack/loaders/babel.js
to translate your ES6 JS back to the stoneage. With Jest, though, it needs a little something more.
This is what my .babelrc
file looks like. Yours will be similar, but your plugins
will be dependent on what you have installed in your react.js
or babel.js
files.
{
"presets": [
"es2015",
"react",
],
"env": {
"test": {
"plugins": [
"transform-function-bind",
"transform-class-properties"
]
}
}
}
Modify the package.json
to include the Jest config
Here is what my diff looked like.
"scripts": {
"eslint": "eslint --ext .jsx --ext .js app/javascript/**"
+ },
+ "jest": {
+ "roots": [
+ "app/javascript"
+ ],
+ "moduleDirectories": [
+ "<rootDir>/node_modules"
+ ],
+ "moduleFileExtensions": [
+ "js",
+ "jsx"
+ ]
}
}
An example
Say you have the files in your project to define the component BlogExample
app/javascript/components/blog_example/index.js
import BlogExample from './blog_example' export default BlogExample
app/javascript/components/blog_example/blog_example.js
import PropTypes from 'prop-types' import React, { Component } from 'react' class BlogExample extends Component { static propTypes = { contacts: PropTypes.arrayOf( PropTypes.shape({ email: PropTypes.string, id: PropTypes.string.isRequired, }).isRequired ).isRequired, handleDeleteContact: PropTypes.func.isRequired, } constructor(props) { super(props) this.renderContacts = this.renderContacts.bind(this) } renderContacts() { return this.props.contacts.map(contact => ( <li key={contact.id}> <a href="#" onClick={() => this.props.handleDeleteContact(contact)}>{contact.email}</a> </li> )) } render() { return ( <div> { this.renderContacts() } </div> ) } } export default BlogExample
You could then create the following base test case
app/javascript/components/blog_example/__tests__/blog_example.spec.jsx
import React from 'react' import renderer from 'react-test-renderer' import BlogExample from '../index' test('Renders contacts', () => { const component = renderer.create( <BlogExample contacts={[{ email: 'jane@example.com', id: '1', }, { email: 'joe@example.com', id: '2', }]} handleDeleteContact={() => {}} /> ) const tree = component.toJSON() expect(tree).toMatchSnapshot() })
If you run this with bin/yarn jest
, you should see the following output generated
PASS app/javascript/components/blog_example/__tests__/blog_example.spec.jsx
✓ Null Contacts (12ms)
› 1 snapshot written.
Snapshot Summary
› 1 snapshot written in 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 added, 1 total
Time: 1.054s
After you have run this, you should see the file app/javascript/components/blog_example/__tests__/__snapshots__/blog_example.spec.jsx.snap
also created.
Say you modify your code base with a change, for example
diff --git a/app/javascript/components/blog_example/blog_example.js b/app/javascript/components/blog_example/blog_example.js
index 1717dab..72bfa39 100644
--- a/app/javascript/components/blog_example/blog_example.js
+++ b/app/javascript/components/blog_example/blog_example.js
@@ -20,7 +20,7 @@ class BlogExample extends Component {
renderContacts() {
return this.props.contacts.map(contact => (
<li key={contact.id}>
- <a href="#" onClick={() => this.props.handleDeleteContact(contact)}>{contact.email}</a>
+ <a href="#" onClick={() => this.props.handleDeleteContact(contact)}>* {contact.email}</a>
</li>
))
}
This will result in a failing test when you run bin/yarn jest
yarn run v1.3.2
warning package.json: No license field
$ /Users/aromeo/workspace/addresser/node_modules/.bin/jest app/javascript/components/blog_example
FAIL app/javascript/components/blog_example/__tests__/blog_example.spec.jsx
✕ Renders contacts (19ms)
● Renders contacts
expect(value).toMatchSnapshot()
Received value does not match stored snapshot 1.
- Snapshot
+ Received
@@ -2,17 +2,19 @@
<li>
<a
href="#"
onClick={[Function]}
>
+ *
jane@example.com
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
+ *
joe@example.com
</a>
</li>
</div>
at Object.<anonymous> (app/javascript/components/blog_example/__tests__/blog_example.spec.jsx:19:16)
at new Promise (<anonymous>)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
› 1 snapshot test failed.
Snapshot Summary
› 1 snapshot test failed in 1 test suite. Inspect your code changes or run with `yarn run jest -- -u` to update them.
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 1 failed, 1 total
Time: 1.324s
Ran all test suites matching /app\/javascript\/components\/blog_example/i.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
As expected with Jest, bin/yarn jest -- -u
updates your snapshot.
Gotchas
yarn
doesn’t verify that the version of react-test-renderer
matches the version of react
you have installed.
"jest": "21.2.1",
- "react-test-renderer": "16.0.0",
+ "react-test-renderer": "^15.5.4",
"redux-devtools": "^3.4.0",
If they don’t match, you’ll get a nasty error… Like this one I got.
yarn run v1.3.2
warning package.json: No license field
$ /Users/aromeo/workspace/addresser/node_modules/.bin/jest app/javascript/components/blog_example
FAIL app/javascript/components/blog_example/__tests__/blog_example.spec.jsx
● Test suite failed to run
TypeError: Cannot read property 'ReactCurrentOwner' of undefined
at node_modules/react-test-renderer/cjs/react-test-renderer.development.js:77:40
at Object.<anonymous> (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7639:5)
at Object.<anonymous> (node_modules/react-test-renderer/index.js:6:20)
at Object.<anonymous> (app/javascript/components/blog_example/__tests__/blog_example.spec.jsx:2:26)
at Generator.next (<anonymous>)
at new Promise (<anonymous>)
at Generator.next (<anonymous>)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 0.764s, estimated 1s
Ran all test suites matching /app\/javascript\/components\/blog_example/i.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.