This is the curriculum for the 2018 Foxtrot Web Developer Bootcamp.

Jest and Enzyme from LEARN on Vimeo

React Testing Tools

There are two main tools you'll use to test your React applications: Jest and Enzyme.

Jest

Jest is a testing framework built right into React when you use create-react-app. Its maintained by the good folks at
Facebook, and they've done a great job of making testing a pleasure as you write your code.

Try this

$ create-react-app jest-example
$ cd jest-example
$ yarn test
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.074s
Ran all test suites related to changed files.

Watch Usage
  Press a to run all tests.
  Press p to filter by a filename regex pattern.
  Press t to filter by a test name regex pattern.
  Press q to quit watch mode.
  Press Enter to trigger a test run.
  • You've just run your first test! *

Notice all of those options above, they are very powerful, and you can use them to run one test, one file of tests, or whatever you'd like. Notice also that when we run yarn test the command doesn't exit back to the command line. This is because Jest continues to run, and will watch your code for changes. If you make a change in either the test, or the file under test, the tests will re-run, automatically! That's pretty cool. A really powerful way to develop is to keep your tests running in a terminal window right inside of Atom. That way you can always see the results of the code changes you make. I use the 'platformio-ide-terminal' package. Here's what my Atom looks like as I write this:

jest in atom

What's in a Test?

Create-react-app adds a testing file for you when you create the app. Let's open it up, and see what it looks like

/src/App.test.js

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

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

1) We need to load the react framework
3) Here we import the App component, which we're going to test
5) This is the first test. Tests are meant to be as human readable as possible. A test has the following structure:

  it("<name of test>", ()=>{
    // Test Setup code
    // Optional expectations we can specify about our code
  })

6) This line is setting up our test, we're working with a real DOM here, actual HTML
7) Finally, in this test, we render our component into the dom, and assure that it doesn't crash.

Adding Enzyme

Jest is pretty easy to use, but adding Enzyme and react-test-renderer makes working with React components even better. First, lets' add them to our package.json file:

$ yarn add -D enzyme react-test-renderer enzyme-adapter-react-16

Now that we've done that, we have some great tools to inspect the rendered HTML of our application. Currently, our App.js file looks like this:

/src/App.js

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

Writing a new Test

Imagine that we want to change the <h2> text to read "Welcome to LEARN". Let's rewrite our test file with a test to check for that, then we'll update the code to make it pass. (that's TDD!)

/src/App.test.js

1  import React from 'react';
2  import ReactDOM from 'react-dom';
3  import App from './App';
4  import Enzyme, { mount } from 'enzyme';
6  import Adapter from 'enzyme-adapter-react-16';
7
8  Enzyme.configure({ adapter: new Adapter() });
9
10 it('Renders a LEARN welcome', ()=>{
11   const app = mount(<App />)
12   expect(app.find('h2').text()).toEqual('Welcome to LEARN')
13 })

** Note: We removed the default test that create-react-app provided, as its not very useful once we're writing our own tests

4) Notice on line 4, we import 'mount' from Enzyme.
6) Here we use mount to instantiate an instance of our App component.
7) Here's the magic of Enzyme, we can call app.find('h2') to inspect the dom, and find our element.
Also notice the syntax of an expectation.

    expect(<one thing>).toEqual(<another thing>)

In this case, we're expecting the things to be the same. We could expect them to be differnt, greater than, or any host of other tests.

Failing test output

Notice the running test command you have, It has notice that we made a code change, and re-run our test for us. ... And we have a failing test! This is a very good thing, as we know what we now need to do to our code.

failing-test

Getting our test GREEN

Our test lets us know exactly what we need to do to get it to pass, so let's do that in App.js

/src/App.js

  // Change this:
  <h2>Welcome to React</h2>

  // To This:
  <h2>Welcome to LEARN</h2>

green

Jest Matchers

There are all kinds of matchers to use with expect. Above we used expect().toEqual(). Checkout Jest's documentation to see them all:

jest matchers

Organizing your tests

One of the great things about Jest is how easy it makes it for us to organize our files. Most tests will be testing code in one file, so we want to keep them as close to each other as possible in our directory. With Jest, we can create a directory anywhere we like, and if we name it 'tests', Jest will look in there and run any tests it fines.

Imagine, as an example our 'jest-example' application. It's file structure currenlty looks like this:

react-files

We can add a 'tests' directory to '/src', and move 'App.test.js' into there. That keeps things organized, and as the numbrer of test files we write grows, they stay tucked away, but easily findable. We can add this special directory anywhere we like in our app, and Jest will know what to do.

tests dir

So, when you want Jest to find your tests, the rule is:

1) use a filename with '.test' in it, "App.test.js" for example.
2) Make a 'tests' directory, and put your tests in there

Interactions from LEARN on Vimeo

Interacting with DOM Elements

To follow along with the video, here is the code for /src/App.js:

/src/App.js

import React, { Component } from 'react';
import {
  Col,
  Button,
  Grid,
  PageHeader,
  Row,
} from 'react-bootstrap'

class App extends Component {
  render() {
    return (
      <Grid>
        <PageHeader>
          Testing Example
        </PageHeader>
        <Row>
          <Col xs={4}>
            <Button>
              Play Game
            </Button>
          </Col>
          <Col xs={8}>
          </Col>
        </Row>
      </Grid>
    );
  }
}

export default App;

And here is the code for /src/__tests__/App.js:

/src/__tests__/App.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

test('button starts off with label "Play Game"')
test('starts out with no text in main container')
test('button changes to "Excellent!" when clicked')
test('displays game results when button clicked')
test('hides game results when results visible and button clicked')
test('changes button text when button clicked a second time')

Often in our tests, we will want to click on buttons, navigate to new pages, and other interactive actions. Enzyme makes this possible by simulating user actions. Here is the test file from the video above:

/src/__tests__/App.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

test('button starts off with label "Play Game"', () =>{
  const app = mount(<App />)
  expect(app.find('button').text()).toEqual('Play Game')
})
test ('starts out with no text in main container', () => {
  const app = mount(<App />)
  expect(app.find('#result-text').text()).toEqual('')
})
test('button changes to "Excellent!" when clicked', () => {
  const app = mount(<App />)
  app.find('button').simulate('click')
  expect(app.find('button').text()).toEqual('Excellent!')
})
test('displays game results when button clicked', () => {
  const app = mount(<App />)
  app.find('button').simulate('click')
  expect(app.find("#result-text").text()).toContain(
    'Congratulations')
})
test('hides game results when results visible and button clicked', () => {
  const app = mount(<App />)
  app.find('button').simulate('click')
  app.find('button').simulate('click')
  expect(app.find('#result-text').text()).toEqual('')
})
test('changes button text when button clicked a second time', () => {
  const app = mount(<App />)
  app.find('button').simulate('click')
  app.find('button').simulate('click')
  expect(app.find('button').text()).toEqual('Play Game')
})

And here is the Component to make these tests pass:

/src/App.js

import React, { Component } from 'react';
import {
  Col,
  Button,
  Grid,
  PageHeader,
  Row,
} from 'react-bootstrap'

class App extends Component {
  constructor(props){
    super(props)
    this.state = {
      buttonText: "Play Game",
      gameWon: false
    }
  }
  toggleResult(){
    const newButtonText = this.state.gameWon ? "Play Game" : "Excellent!"
    const newGameWon = this.state.gameWon ? false : true
    this.setState({ buttonText: newButtonText, gameWon: newGameWon})

  }
  render() {
    return (
      <Grid>
        <PageHeader>
          Testing Example
        </PageHeader>
        <Row>
          <Col xs={4}>
            <Button onClick={this.toggleResult.bind(this)}>
              {this.state.buttonText}
            </Button>
          </Col>
          <Col xs={8}>
            <span id="result-text">
              {this.state.gameWon &&
                <h4>Congratulations!  You have won a free book about React!</h4>
              }
            </span>
          </Col>
        </Row>
      </Grid>
    );
  }
}

export default App;

Introduction

Let's refer back to the wireframes, and recall what our interface is going to look like:

wires

App setup

Using create-react-app, react-router-dom and react-bootstrap, we can setup a new application:

$ create-react-app cat-tinder-frontend
$ cd cat-tinder-frontend
$ yarn add react-bootstrap react-router-dom
$ yarn add -D enzyme react-test-renderer enzyme-adapter-react-16

Add a theme

I'm going to use the "United" theme from bootswatch.com, so I'll add the stylesheet to 'pubic/index.html' You can download a theme from here: Bootswatch and put it in the public/ directory.

public/index.html

<link rel="stylesheet" href="%PUBLIC_URL%/bootstrap.min.css">

A good starting point

Create React App gives us a file called App.js that ties in to the Index.js file, it will be the entry point of our app.

Just like when we built rails with views, we want to put our router out in front of our application, to help us process requests from the user. So, our React Router logic will live here in our App.js file.

src/App.js

import React, { Component } from 'react'
import { BrowserRouter as Router, Switch, Route } from react-router-dom'

import Cats from ./pages/Cats
import NewCat from ./pages/NewCat

class App extends Component {
  render() {
    return (
        <div>
            <Header />
            <Router>
                <Switch>
                    <Route exact path="/cats" component={Cats} />
                    <Route exact path="/" component={NewCat} />
                </Switch>
            </Router>
        </div>
    );
  }
}

export default App;

We havent really built any components yet, so this code will throw an error, but we have set up our basis for handling requests.

Challenge

Did you see that we are using a Header component that hasn't been created anywhere?

Make a header component with bootstrap and add it to the project as src/components/Header.js.

Remember that running yarn start will error out until we create the Cats and NewCat components. Feel free to put placeholders there while you create your header.

Cats Component

Its time to turn our attention to the page components of the application. We'll start with the cats index page and some fake data so that we can get the look of the page correct.

Here's the basic test to start us out:

src/pages/__tests__/Cats.js

import React from 'react'
import ReactDOM from 'react-dom'
import Cats from '../Cats'
import { mount } from 'enzyme'
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'

Enzyme.configure({ adapter: new Adapter() });

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

That will fail until we create the component

src/pages/Cats.js

import React, { Component } from 'react';
import {
  Grid, Col, Row
} from 'react-bootstrap'

class Cats extends Component {
  render() {
    return (
    <Grid>
      <Row>
        <Col>
            <div>I'm a component</div>
        </Col>
      </Row>
    <Grid>
    );
  }
}

export default Cats;

Now that test should pass, because we have created a component that can be rendered. (Meaning that it imports react and has a render function, not that it shows real content.)

But lets fix that by adding some fake cat data to play with. Later this information will come from the rails backend, but for now lets just get something up that we can see and work with.

We want all of our data in a central place, so instead of placing it directly in the pages/Cats.js component, we will put it in our logic component App.js

src/App.js

constructor(props){
    super(props)
    this.state = {
      cats: [
        {
          id: 1,
          name: 'Morris',
          age: 2,
          enjoys: "Long walks on the beach."
        },
        {
          id: 2,
          name: 'Paws',
          age: 4,
          enjoys: "Snuggling by the fire."
        },
        {
          id: 3,
          name: 'Mr. Meowsalot',
          age: 12,
          enjoys: "Being in charge."
        }
      ]
    }
  }

Now we need to send this cats json array to the Cats component as props from App.js. To do this, we need a slightly different syntax in our Router. Change the Cats route to look like this:

<Route exact path="/cats" render={(props) => <Cats cats={this.state.cats}/>} />

Now that our Cats.js component is receiving an array of cats in props, lets add some bootstrap code to create real content in the Cats.js render function, replacing the blank elements we had before.

What else do you have to change about your page to make this work?

pages/Cats.js

<Row>
    <Col xs={12}>
            <ListGroup>
            {this.props.cats.map((cat, index) =>{
              return (
                <ListGroupItem
                  key={index}
                  header={
                    <h4>
                      <span className='cat-name'>
                        {cat.name}
                      </span>
                      - <small className='cat-age'>{cat.age} years old</small>
                    </h4>
                  }>
                  <span className='cat-enjoys'>
                    {cat.enjoys}
                  </span>
                </ListGroupItem>
              )
            })}
          </ListGroup>
        </Col>
      </Row>

Finishing the test

Now we get to test the information in our Cats.js component. Problem, now that the Cats.js takes in props from App.js how can we test that? Our Cats.js component requires that information in order to render.

We need our test to send some json data to pages/Cats.js the same way that App.js is currently sending the cats json as props to pages/Cats.js. It is really convenient if our test uses the same fake data as we have in App.js state.

Below, youll notice that were using an import statement for a thing called mount from Enzyme. It will allow us to pass information to a component we are testing.

Write some tests to cover the content we just added to Cats.js.

src/pages/__tests__/Cats.js

const cats = [
  {
    id: 1,
    name: 'Morris',
    age: 2,
    enjoys: "Long walks on the beach."
  },
  {
    id: 2,
    name: 'Paws',
    age: 4,
    enjoys: "Snuggling by the fire."
  },
  {
    id: 3,
    name: 'Mr. Meowsalot',
    age: 12,
    enjoys: "Being in charge."
  }
]

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

it('Renders the cats', ()=>{
  const component = mount(<Cats cats={cats} />)
  const headings = component.find('h4 > .cat-name')
  expect(headings.length).toBe(3)
})

Try those tests, they should be green.

Challenge

Now, try adding some more tests of your own.

NewCat Component

Time to build the form to add new cats. Remember we already have the route for NewCat in App.js - so all thats left is to create the component.

Challenge

Your challenge this time is to create a component that fulfills the tests in this file. You will see that these tests assume we are using bootstrap to create our view, and reference bootstrap components that will need to be added to your pages/Cats.js file.

src/pages/__tests__/NewCat.js

import React from 'react'
import ReactDOM from 'react-dom'
import NewCat from '../NewCat'
import { mount } from 'enzyme'
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'

Enzyme.configure({ adapter: new Adapter() });

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

it('has a name input', ()=>{
  const component = mount(<NewCat />)
 expect(component.find('label#name').text()).toBe("Name")
})

it('has a age input', ()=>{
  const component = mount(<NewCat />)
  expect(component.find('label#age').text()).toBe("Age")
})

it('has a enjoys input', ()=>{
  const component = mount(<NewCat />)
  expect(component.find('label#enjoys').text()).toBe("Enjoys")
})

it('has a submit button', ()=>{
  const component = mount(<NewCat />)
  expect(component.find('button#submit').text()).toBe("Create Cat Profile")
})

Once you have a view that satisfies the tests, youll be ready to refactor your code to create a controlled form.

Controlled components

Thinking ahead just a bit, we're going to need to pass the values from our form back up to the calling component. In order to do this easily, we will hold the values typed in by the user in state. To watch our inputs and save values into state, we need to switch our inputs to being controlled components(meaning watched by state). Or, in other words, add a 'value', and an 'onChange' attribute to the inputs. Then we can manage the value of the inputs in the components internal state until the form is submitted. Do you remember how to do this from the "Component, State, & Props" day?

We start by adding state to the component in a constructor:

src/pages/NewCat.js

constructor(props){
  super(props)
  this.state = {
    form:{
      name: '',
      age: '',
      enjoys: ''
    }
  }
}

And then for each input, we bind its value to state. We'll add a name to the input too, and an 'onChange()' callback, as we're going to need those next. Here is 'name', the other two are nearly identical.

src/pages/NewCat.js

<FormControl
  type="text"
  name="name"
  onChange={this.handleChange.bind(this)}
  value={this.state.form.name}
/>

So what does handleChange() look like?

src/pages/NewCat.js

handleChange(event){
  let {form } = this.state
  form[event.target.name] = event.target.value
  this.setState({form: form})
}

For discussion

Notice how we didn't test the 'handleChange()' method and when it was called? Why do you suppose we didn't do that?

The answer is that handleChange() is an internal mechanism of the component, and we want to have flexibility later down the road to change how the component works. We're not particularily interested in those inner workings from a testing perspective.

What we are interested in is what the component passes back to its caller, which we're going to test extensivly. If you remember that testing is for validating outputs based on particular inputs, you'll write flexible tests that allow you to easily refactor.

As a General Rule:

  • Don't test the inner working of a component.
  • Test that you get the correct outputs based on specific inputs.
  • Test behavior of the component, especially if it directly affects the user experience.

NEWCAT FUNCTIONALITY

Now that the NewCat form is rendering correctly, we can wire the form up to pass submitted data to App.js, and handle errors correctly.

What happens when a user clicks "Create Cat Profile" ?

When a user clicks the Create a Cat button to submit the form, we want that to trigger a series of actions that lead to adding a new cat to the rails database. Lets chart where the information submitted by the user has to travel in order to get to the db.

Flow of information from form to DB

1. Preparation

in App.js

A function is created to handle information for a new cat. That function is preemptively sent to the dumb component (in our case pages/NewCat.js) as props. This function is saying to the dumb component: Here is a function that takes in new cat json as an argument. Only run it when you have all the information for a new cat.

2. Data Entry

in pages/NewCat.js

User inputs data into the form. The form is controlled, and saves all data directly into state

3. Form Submit

in pages/NewCat.js

User hits submit - this is our signal that they are finished filling in their information. The form runs an onClick function, which calls the function held in props from App.js. That function runs and sends the form data (currently in state) to the logic component (in our case App.js)

4. Pass Data to the API

in App.js

We now would trigger a POST type request to the backend, and rails would handle the rest but more on that tomorrow.

Challenge

Create a handleNewCat function in App.js that only does one thing: console.log the information from the NewCat form. Pass the handleNewCat function to the NewCat component as props, and then run the handleNewCat function when a user clicks the form submit button

Today's Tentative Schedule

9:15am - Stand Up

9:30am - BDD with Models, Database Generators and Active Record Validations

11:00am - Challenge: Create Databases

12:00 noon - Lunch

1:00pm - continue with Task List with SQL Challenge

4.30pm - Review

5:00pm - Class Ends