Elvenware

JestCreateReactApp

Welcome to JestCreateReactApp

Overview

By: Charlie Calvert

Learn how to write Jest tests with the npm packaged called create-react-app.

A deck to accompany this assignment is here:

Install

create-react-app is probably already on your system in ~/npm/bin. That directory should be on your path in the default Pristine Lubuntu system.

If create-react-app is not on your system, then install it like this:

npm install -g create-react-app

Make sure you are on the latest version:

npm -g outdated

If any globally installed apps are outdated, then reinstall them:

npm install -g create-react-app

Set the Default Port

Load your .bashrc file into an editor and add this line near the bottom of the file, if it is not already there:

export PORT=30025

Some of our programs will use this as the default PORT on which to run.

NOTE: We might want to run some programs, especially Express programs, on some other port than 30025, such as 30026. Note that they will default to the port set in the manner described above. As a result, we will sometimes need to take steps to ensure they run on the port we want. One technique is to define the port in package.json:

"config": {
    "port": "30026"
},

Then in www/bin:

const port = process.env.npm_package_config_port || 30026;

Get Started

Switch to your Week01 branch.

Running Tests

Open up src/App.test.js, shown below. This is a test that allows you to see if the syntax in App.js is good enough to allow the component to be loaded.

To run the tests, type: npm test in the project's root directory.

Jest tests are in files with these extensions:

Here is the simple default test generated by create-react-app:

import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';

it('renders without crashing', () => {
   const div = document.createElement('div');
   ReactDOM.render(<App />, div);
});

This test is built with Jest library, but the syntax looks just like the very popular Jasmine library. In most cases, it also behaves much like Jasmine.

Writing Tests

Let's update the default example by wrapping the test in a describe method:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

describe('Jest Create React Tests', function () {

   it('renders without crashing', () => {
       const div = document.createElement('div');
       ReactDOM.render(<App />, div);
   });

});

Enzyme

We need a tool to capture and parse the output created by our React components. We test that output to see if it is valid. The tool that helps us do this is airbnb's Enzyme.

To install it run these commands:

npm install --save-dev enzyme react-test-renderer
npm install --save-dev enzyme-adapter-react-16

I believe we no longer need to use react-addons-test-utils

Enzyme Test of Component Output

The updated tests shown below import shallow from enzyme. It grabs text from our component and then checks to see if the text is what we expect it to be.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Adapter from 'enzyme-adapter-react-16';
import { configure, shallow } from 'enzyme';
configure({ adapter: new Adapter() });

describe('jest test', function() {

   it('renders without crashing', () => {
       const div = document.createElement('div');
       ReactDOM.render(<App />, div);
       ReactDOM.unmountComponentAtNode(div);
   });

   it('renders and reads H1 text', () => {
       const wrapper = shallow(<App />);
       const welcome = <h2>Welcome to React</h2>;
       expect(wrapper.contains(welcome)).toEqual(true);
   });

});

Perhaps you are getting an error when you run these tests. You may be able to fix it by yourself, but let's learn how to use ElfDebugEnzyme to help you find the error.

Elf Debug Enzyme

Get a copy of my gist ElfDebugEnzyme and save it to a file with the same name (ElfDebugEnzyme.js):

One way to save the file is to the choose the Raw button on GitHub, and then blockcopy the code. Now create a new file in WebStorm and paste in the code.

Now import ElfDebugEnzyme into your test:

import ElfDebugEnzyme from '../ElfDebugEnzyme';

const elfDebugEnzyme = new ElfDebugEnzyme(true, 'App.test.js');

Use ElfDebugEnzyme to display the first H1 element:

it('renders and reads H1 text', () => {
    const wrapper = shallow(<App />);
    const welcome = <h1 className="App-title">Welcome to React</h1>;
    elfDebugEnzyme.getFirst(wrapper, 'h1', true);
    expect(wrapper.contains(welcome)).toEqual(true);
});

The key line is this one:

elfDebugEnzyme.getFirst(wrapper, 'h1', true);

It calls the getFirst method of ElfDebugEnzyme and and asks to see the contents of the first H1 element generated by your React component. In our case, there is only one H1 element, and it looks like this, when printed out by ElfDebugEnzyme:

console.log ElfDebugEnzyme.js:45
  App.test.js:
  <h1 className="App-title">
    Welcome to React
  </h1>

The relevant code in ElfDebugEnzyme is simple enough. It asks enzyme to find the first matching H1 element and display the debug information associated with that element:

wrapper.find(element).first().debug();

Once you know the HTML that is being generated, you can adjust your test to ensure that it properly matches the HTML. Or vice versa, depending on your needs and circumstances.

Add a constructor and declare some state.

Once you know how to test for static HTML generated by your React component, then next step will be to test the dynamic code, the code that changes when you -- for instance -- press a button. Let's begin by adding a constructor to your React component.

The constructor is a function on your component. React calls is it before it mounts the component. Call super() first or else the this variable will not be valid in the constructor.

class App extends Component {
   constructor() {
       super();
       this.state = {
           file: 'unknown'
       }
   }
… etc
}

React will keep your state variables up to date in your UI if you display and play by certain rules. In particular, when you change these variable, use setState as described later in this chapter.

In render, display the state

In our JSX, we:

<p className="App-intro">File: {this.state.file}</p>

Define a function called getFile

We declare an arrow function function in our component called getFile. Inside it, we call setState. The setState call can take an object literal defining the new state.

getFile = () => {
    console.log('getFile called.');
    this.setState({file: 'url-file.js'})
};

In render, display the state

In our JSX, we:

<button className='elf' onClick={this.getFile}>Get Nine</button>

Test button click

It's time to write some unit tests with Jest.

Call the enzyme simulate method to simulate clicking the button. Check to see if the form now contains the value we expect it to contain. In other words, check that the button click method worked.

it('renders button click message', () => {
   const wrapper = shallow(<App />);
   const nineSign = <p className="App-intro">File: url-file.js</p>;
   wrapper.find('button.elf').simulate('click');
   expect(wrapper.contains(nineSign)).toEqual(true);
});

To run only a single test use it.only or fit:

it.only('renders button click message', () => {
   const wrapper = shallow(<App />);
   etc...   
})

With only:

it.only('renders button click message', () => {
   const wrapper = shallow(<App />);
   etc...
})

NOTE: When I write something like etc... or and so on... or You write the code, then I'm saying that I expect you to complete the code as an exercise. I usually cut and paste working code into a document like this, and then delete the parts I want readers to complete. I usually mark the missing code as described above.

ESLint

Eslint should be installed globally in ~./npm/bin.

Add this .eslintrc.json file to your project.

{
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended"
    ],
    "rules": {
        // enable additional rules
        "indent": ["error", 4],
        "linebreak-style": ["error", "unix"],
        "quotes": ["warn", "single"],
        "semi": ["error", "always"],

        // override default options for rules from base configurations
        "comma-dangle": ["off", "always"],
        "no-cond-assign": ["error", "always"],

        // disable rules from base configurations
        "no-console": "off"
    },
    "parserOptions": {
        "ecmaVersion": 7,
        "sourceType": "module",
        "ecmaFeatures": {
            "jsx": true
        }
    },
    "env": {
        "browser": true,
        "node": true
    }
}

Run it like this:

eslint%20.

The code above sometimes needs to be updated. To see my current working .eslintrc.json file, go here.

Turn it in

Place your work in your repository if it is not already there. Merge your finished project into master.

Push your repository. Go to GitHub or BitBucket and ensure that the code you want to turn in is actually in your repository and that it contains the files and folders you expect it to contain.

Find the assignment on Canvas and submit it. Add text that states the name of the folder where you placed your assignment. A link to your folder on GitHub/Bitbucket is nice.

For most of the assignments, I'll just say something like: "Put your work in your repo and push," or simply "Push your work". That's a shorthand for something along the lines of what I'm saying here.

Test Output

I'm expecting the test output to look something like this:

PASS  src/App.test.js
 jest test
   ✓ renders without crashing (4ms)
   ✓ renders and reads H1 text (3ms)
   ✓ renders button click message (1ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.364s, estimated 1s
Ran all test suites.

 console.log ElfDebugEnzyme.js:45
   App.test.js:
   <h1 className="App-title">
     Welcome to React
   </h1>

 console.log src/App.js:14
   state

 console.log ElfDebugEnzyme.js:45
   App.test.js:
   App.test.js:
   <p className="App-intro">
     File:
     'url-file.js'
   </p>

Hint

Don't nest folders! There should be only one folder called Week02-ReactJest in your repository. It should be in the root of your repository.

The warnings

You are probably no longer getting these warnings, but just in case I will include a few notes about warnings I received.

Warning: ReactTestUtils has been moved to react-dom/test-utils. Update references to remove this warning. Warning: Shallow renderer has been moved to react-test-renderer/shallow. Update references to remove this warning

For awhile, I was seeing warnings due to outdated react-addons-test-utils and enzyme modules. I believe Enzyme has solved these issues. If you see the warnings below, try updating these two packages:

npm uninstall react-addons-test-utils enzyme
npm install react-addons-test-utils enzyme