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 using- babel, this is the associated- jestplugin)
- 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.