Testing React.js components with Jest in Rails+Webpacker+Webpack environment

Around a month ago, I worked on a task, which required a more dynamic frontend behavior. I worked on a component with 2 selects and 2 date pickers and depending on what was selected where the other pickers or select inputs had to be updated based on some relatively simple business rules. I decided to implement it using React.js and it was fun and pretty straight-forward to finish it. Also, working with http://airbnb.io/react-dates/ turned out to be a very pleasureful experience. But that’s not what this post is about.

I wanted to test my component. The integration between Rails asset pipeline (which you can find in almost all legacy Rails apps) and Webpack (which is what anyone wants to use nowadays) is called Webpacker. Thanks to it you can organize, manage, compile your new JavaScript code with Webpack and have it nicely integrated into whole Rails app deployment process. For testing, I wanted to use Jest, which I prefer more than its alternatives.

There are already guides on how to achieve that and that’s what I started with. But it was not good enough for me to end up with a working solution. I had to do much more manual work and googling than I expected. So I decided to document the process hoping that it will make other people’s life a bit easier.

BTW. I don’t have a PhD in Webpack so forgive me if all of that is obvious to you.

Preconditions

Install Jest

yarn add --dev jest

Now we can configure commands for running jest and tell where we keep our test files by adding those lines to package.json.

  "scripts": {
    "test": "jest",
    "test-watch": "jest --watch"
  },
  "jest": {
    "roots": [
      "test/javascript"
    ]
  }

This project uses MiniTest and test/* directories for Ruby tests and I decided to add my tests in similar test/javascript location.

I added a very simple check in test/javascript/sum.test.js to verify that this step of the configuration is working correctly.

test('1 + 1 equals 2', () => {
  expect(1 + 1).toBe(2);
});

Run yarn test and you should that it works.

It doesn’t mean much but at least I knew I had jest installed and working for most simple cases. That’s something.

Setup Babel

This step is required to have import directives working.

Run:

yarn add --dev babel-jest regenerator-runtime

Now, here is something other tutorials did not mention, but which is mentioned in https://github.com/facebook/jest#additional-configuration

If you’ve turned off transpilation of ES modules with the option { “modules”: false }, you have to make sure to turn this on in your test environment.

And indeed. Webpacker initially creates a .babelrc configuration:

  "presets": [
    [
      "env",
      {
        "modules": false,
        "targets": {
          "browsers": "> 1%",
          "uglify": true
        },
        "useBuiltIns": true
      }
    ],
    "react"
  ],

which disables transpilation of ES modules. So we need to overwrite the configuration for test env:

{
  "env": {
    "test": {
      "presets": [["env"], "react"]
    }
  }
}

Why by default (webpacker’s default) does the configuration say "modules": false and what does it mean? That’s a really long story… It is necessary for tree shaking, webpack 2 can do it only with ES6 modules syntax. What is tree shaking? It’s eliminating unused code that is not imported. Wait, there are many modules syntaxes? Yes, there are and only some of them are statically analyzable. I guess you know that and it is obvious if you come from JS community but coming from Ruby community I really needed to educate myself as to what and why to understand what I am doing here.

Also, there is no need to use babel-preset-es2015 as recommended in some articles.

Without any configuration options, babel-preset-env (which we have in config) behaves exactly the same as babel-preset-latest (or babel-preset-es2015, babel-preset-es2016, and babel-preset-es2017 together). For more information on that check out: https://babeljs.io/docs/plugins/preset-env/

To verify that I could use import I used test/javascript/sum.test.js:

import _ from 'lodash';
import moment from 'moment';

test('1 + 1 equals 2', () => {
  expect(1 + 1).toBe(2);
  console.log(moment());
});

And it worked. Bear in mind that I already had lodash and moment.js installed with yarn.

P.S.

"presets": [["env",

have nothing to do with

  "env": {
    "test":

These two envs have 2 different meanings. That’s confusing when you see:

{
  "presets": [["env", {"modules": false}], "react"],
  "env": {
    "test": {
      "presets": [["env"], "react"]
    }
  }
}

"presets": [["env"] is about https://babeljs.io/docs/plugins/preset-env/ and "env":{"test": (which can overwrite presets) is about https://babeljs.io/docs/usage/babelrc/#env-option (supporting BABEL_ENV and NODE_ENV environment variable for overwriting configuration).

Configure Jest to find modules

The moduleDirectories setting can be used to tell Jest where to look for modules.

I configured it to use like that:

  "jest": {
    "moduleDirectories": [
      "node_modules",
      "app/javascript/packs"
    ]
  }
}

I verified that I can use JSX syntax in test/javascript/sum.test.js with:

import _ from 'lodash';
import moment from 'moment';

import React from 'react';
import ReactDOM from 'react-dom';

test('1 + 1 equals 2', () => {
  expect(1 + 1).toBe(2);
  console.log(moment());
  const asd = <div>asd</div>;
});

and it worked. Nothing crashed.

Enzyme

For testing react components I like to use Enzyme

yarn -add enzyme enzyme-adapter-react-16 jest-enzyme

More on that in http://airbnb.io/enzyme/docs/installation/ and https://github.com/airbnb/enzyme/blob/master/docs/guides/jest.md and https://github.com/FormidableLabs/enzyme-matchers/tree/master/packages/jest-enzyme#setup

In package.json I added

{
  "jest": {
    "setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js",
  }
}

No idea, why this line is needed.

That was not enough yet for testing my React components though. I still had some failures.

Handle CSS in testing React component

Based on webpack instructions instructions. I didn’t go with Mocking CSS modules. That was not necessary for me.

Let’s configure Jest to gracefully handle asset files such as stylesheets and images. Usually, these files aren’t particularly useful in tests so we can safely mock them out.

In package.json we add:

{   
  "jest": {
    "moduleNameMapper": {
      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
      "<rootDir>/test/javascript/__mocks__/fileMock.js",
      "\\.(css|less)$": "<rootDir>/test/javascript/__mocks__/styleMock.js"
    }
  }
}

I created: test/javascript/__mocks__/fileMock.js with:

module.exports = 'test-file-stub';

and test/javascript/__mocks__/styleMock.js

with

module.exports = {};

I seriously did not expect that such configs will be necessary…

React+JSX+Enzyme test

With that I could finally test my first component:

import React from 'react';
import ReactDOM from 'react-dom';

import { shallow, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

import 'react-dates/initialize';
import AdvancedSearchSeasonOptions from 'advances_search_season_options/component';

test('rendered component', () => {
  const wrapper = shallow(<AdvancedSearchSeasonOptions
    season_available={null}
    year_available={null}
    release_on_from={null}
    release_on_to={null}
    seasons={[
      {"year":2018,
       "season":"spring",
       "yearSeason":"2018-spring",
       "begin":"2017-12-15",
       "end":"2018-07-31",
       "name":"Spring 2018"
      },
    ]}
    years={[
      {"year":2018,"begin":"2017-12-15","end":"2018-12-14"},
    ]}
  />);
  expect(wrapper.find('div.half.first')).toHaveLength(2);
});

Notice that I needed import 'react-dates/initialize'; only because I am using react-dates component. Your first component will most likely not need it.

Summary

That’s more or less how started testing our React.js components with Jest in Rails apps that use Webpacker to integrate with Webpack. I definitely thought more of those things would work out of the box and without me having to understand all of that details.

Current status

Let me show you full .babelrc:

{
  "presets": [
    [
      "env",
      {
        "modules": false,
        "targets": {
          "browsers": "> 1%",
          "uglify": true
        },
        "useBuiltIns": true
      }
    ],
    "react"
  ],
  "env": {
    "test": {
      "presets": [
        [
          "env"
        ],
        "react"
      ],
      "plugins": [
        "syntax-dynamic-import",
        "transform-object-rest-spread",
        [
          "transform-class-properties",
          {
            "spec": true
          }
        ]
      ]
    }
  },
  "plugins": [
    "syntax-dynamic-import",
    "transform-object-rest-spread",
    [
      "transform-class-properties",
      {
        "spec": true
      }
    ]
  ]
}

and almost full package.json:

{
  "dependencies": {
    "@rails/webpacker": "^3.2.1",
    "babel-preset-react": "^6.24.1",
    "camelize": "^1.0.0",
    "classnames": "^2.2.5",
    "lodash": "^4.17.4",
    "mailcheck": "^1.1.1",
    "prop-types": "^15.6.0",
    "react": "^16.2.0",
    "react-async-script": "^0.9.1",
    "react-dom": "^16.2.0",
  },
  "devDependencies": {
    "babel-jest": "^22.1.0",
    "babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
    "enzyme": "^3.3.0",
    "enzyme-adapter-react-16": "^1.1.1",
    "jest": "^22.1.1",
    "jest-enzyme": "^4.0.2",
    "regenerator-runtime": "^0.11.1",
    "webpack-dev-server": "^2.11.1"
  },
  "scripts": {
    "test": "jest",
    "test-watch": "jest --watch"
  },
  "jest": {
    "setupTestFrameworkScriptFile": "./node_modules/jest-enzyme/lib/index.js",
    "moduleNameMapper": {
      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/javascript/__mocks__/fileMock.js",
      "\\.(css|scss|less)$": "<rootDir>/test/javascript/__mocks__/styleMock.js"
    },
    "roots": [
      "app/javascript/packs",
      "test/javascript"
    ],
    "moduleDirectories": [
      "node_modules",
      "app/javascript/packs"
    ]
  }
}

after a couple of other problems that we discovered later and had to handle as well.

Bonus - running JS tests on CircleCI 2.0

This also turned out to be a bit more tricky than I expected. The reason for that is that running assets pre-compilation (asset pipeline+webpack via webpacker integration) or running tests uninstalled devDependencies and then we could not run jest because there was no such binary. Rails probably called yarn with some options which lead to uninstalling jest. I totally did not expect that behavior.

Here is our current config:

version: 2
jobs:
  build:
    docker:
      - image: circleci/ruby:2.2.7-node
        environment:
          RAILS_ENV: test
      - image: circleci/mysql:5.5
        environment:
          - MYSQL_ROOT_PASSWORD=ubuntu
          - MYSQL_DATABASE=myapp-test
      - image: elasticsearch:1.4.5

    working_directory: ~/repo

    steps:
      - checkout
      - run: mkdir -p tmp/
      - run: mkdir -p ~/ftp/

      - restore_cache:
          keys:
          - v2-project-{{ checksum "Gemfile.lock" }}
          - v2-project-

      - run:
          name: install dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path bundled-gems

      - save_cache:
          paths:
            - bundled-gems
          key: v2-project-{{ checksum "Gemfile.lock" }}

      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      - run: bundle exec rake assets:precompile

      - run:
          name: run tests
          command: |
            ./bin/rails t

      - restore_cache:
          keys:
          - yarn-npm-packages-2-{{ checksum "yarn.lock" }}
          - yarn-npm-packages-2-

      - run:
          name: install js dependencies
          command: |
            yarn install --cache-folder ~/.cache/yarn --production=false

      - save_cache:
          paths:
            - ~/.cache/yarn
          key: yarn-npm-packages-2-{{ checksum "yarn.lock" }}

      - run:
          name: run js tests
          command: |
            yarn test

I think eventually I will either move JS testing before assets pre-compilation and rails testing. Alternatively, I am going to split this one long job into a workflow with two separate jobs. Especially considering that 90% of commands (some not listed for clarity) do not affect JS testing at all.

Another thing that trolled me a little was setting NODE_ENV globally to production, which I tried in the beginning. This caused more issues than problems that it solved. Do no do it ๐Ÿ˜‰

That’s it folks. Please test your JavaScript.

P.S. I hope at least some of those configs won’t be necessary with Webpack 4 more configless approach.

Read more

If you enjoyed that story, subscribe to our newsletter. We share our everyday struggles and solutions for building maintainable Rails and React apps which don’t surprise you.

Also worth reading:

And don’t forget to check out our React books Rails meets React and React.js by example. Both helped thousands of React and Rails developers.