Testing React.js components with Jest in Rails+Webpacker+Webpack environment
… and check why 5600+ Rails engineers read also this
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
- You have Rails 5 app
You have webpacker installed in that Rails 5 app with React.js integration
Add to your
Gemfile
gem 'webpacker', '~> 3.2'
Run:
bundle install bundle exec rails webpacker:install
Run:
bundle exec rails webpacker:install:react
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 env
s 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:
- How we’ve updated React by Example from React 0.13 to 16.0 - React by Example as a book is focused on 12 different examples of UI widgets. Every chapter is a walkthrough on implementing such component, whether itโs a password strength meter or article list with voting. Some time ago we updated its code to React 16.
- Diving into ant-design internals: Button - Check out how ant, one of the biggest collection of cohesive React components, implemented interesting features in their buttons.
- Dynamic JSX tags - very quick protip how to achieve conditional JSX tags with much shorter syntax.
- Mapping declarative React components to imperative external API. - Did you like Netflix article on Integrating imperative APIs into a React application? Check out our similar approach to a similar problem.
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.