Skip to content

Instantly share code, notes, and snippets.

@leahgarrett
Last active July 2, 2019 00:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leahgarrett/3bd31429fecc898e00c3ddc39e87dad1 to your computer and use it in GitHub Desktop.
Save leahgarrett/3bd31429fecc898e00c3ddc39e87dad1 to your computer and use it in GitHub Desktop.
Testing React

Testing React

Today we will be testing the Book app. Either use your version and add the BookList component or clone my version and code along.


Creating a BookList component

Code for this walk through:
https://github.com/leahgarrett/books-front-end-mern

We have created a BookList component that will be easier to test.

/src/component/BookList.js

import React, { Component } from 'react';
import './BookList.css';

class BookList extends Component {
  constructor(props) {
      super(props);
      this.state = { 
          books: []
      };
    }

    handleClickBook = (book) => {
      console.log(book)
      this.props.handleEditClick(book)
  }


  render(){
    console.log(this.state);
      return (
        <div className="book-list">
        {this.props.books.map((item, index) => (
            <div onClick={() => this.handleClickBook(item)} key={index} className={item.genre}>
            {item.genre}: {item.title} by {item.author} (rrp ${item.price}) (id: {item.id})
            </div>
        ))}
      </div>
      );
    }
}


  export default BookList;

Styling for the new component includes the challenge requirement to color the background of the books in the book list by genre.

/src/component/BookList.css

.book-list {
    padding: 20px 0;
}
.book-list  div {
    padding: 7px;
    margin: 5px 0;
    border: 1px solid #ccc;
    color: white;

}
.comedy {
    background-color: darkmagenta;
    
  }

  .drama {
      background-color: maroon;
  }

  .action {
    background-color: darkblue;
  }

  .thriller {
    background-color: darkgreen;
  }

The render method has been updated:

/src/App.js

  render() {
    console.log('App is running render');
    return (
      <div>
        {this.state.adding ? 
         <BookForm addNewBook={this.addNewBookHandler} /> : 
          this.state.selectedBook ? 
            <BookForm 
                addNewBook={this.addNewBookHandler} 
                title={this.state.selectedBook.title} 
                author={this.state.selectedBook.author} 
                genre={this.state.selectedBook.genre} 
                price={this.state.selectedBook.price} 
                id={this.state.selectedBook.id} 
            /> : 
            <>
                <h1>Books {this.state.books.length}</h1>
                <button onClick={this.handleClick}>Add</button>
                <BookList books={this.state.books} handleEditClick={this.handleEditClick} />
            </>
        }
      </div>
    );
  }

To check that is is working

Run the front-end

Change into the app folder
cd books-front-end

Install dependencies
npm install

Run the app
npm start

Run the back-end

Change into the directory
cd books-backend-mern

Run the server
npm start

The back-end is running on port 5000. Our front-end will run on port 3000.

To seed the data
http://localhost:5000/seed

To view the data
http://localhost:5000/books


Installing dependencies for testing

In the folder with the front-end
npm install enzyme --save-dev

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

npm install enzyme-to-json --save-dev

npm install react-test-renderer --save-dev

npm install sinon --save-dev


The --save-dev flag will put these changes in the devDependencies section in package.json. Your package.json should look like:

  "devDependencies": {
    "enzyme": "^3.10.0",
    "enzyme-adapter-react-16": "^1.14.0",
    "enzyme-to-json": "^3.3.5",
    "jest": "^24.8.0",
    "react-test-renderer": "^16.8.6",
    "sinon": "^7.3.2"
  }

Setting up for tests

In the src folder make a new file called setupTests.js

/src/setupTests.js

import React from "react";
import Enzyme, { shallow, render, mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { createSerializer } from "enzyme-to-json";
import sinon from "sinon";

// Set the default serializer for Jest to be the from enzyme-to-json
// This produces an easier to read (for humans) serialized format.
expect.addSnapshotSerializer(createSerializer({ mode: "deep" }));

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

Writing our first test

describe wraps a group of related tests. In our case the component tests.

it is the function you write anytime you want to write a test. The string should explain what is being tested.

/src/components/BookList.test.js

import BookList from "./BookList";

describe("<BookList />", () => {
  it("correctly adds numbers", () => {
    expect(2 + 2).toBe(4);
  });
});

Adding render tests for books

/src/components/BookList.test.js

import BookList from "./BookList";

describe("<BookList />", () => {
    it("renders component", () => {
    const books = [];
    const wrapper = shallow(
        <BookList books={books} handleEditClick={() => {}} />
    );
    expect(wrapper).toMatchSnapshot();
    });
});

The tests will pass:

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   1 passed, 1 total
Time:        2.015s
Ran all test suites.

Watch Usage: Press w to show more.

The first time the tests are run the snapshot is written.

Let's have a look at the snapshot that was created:
src/components/__snapshots__/BookList.test.js.snap


Let's add another test. We will create an array called books3 which we will use in later tests, so we will create the array outside of our test so we can share it.

/src/components/BookList.test.js

import BookList from "./BookList";

describe("<BookList />", () => {
    const books3 = [{
    author: "Ms. Remington Fahey",
    genre: "comedy",
    id: 1,
    price: 53.7,
    title: "Legacy Intelligent",
    _id: "5d19cd74a0c3e2c7f68ff800"
    },
    {
    author: "Isaac Steuber",
    genre: "thriller",
    id: 2,
    price: 20.99,
    title: "Incredible Incredible Plastic Computer",
    _id: "5d19cd74a0c3e2c7f68ff801"
    },
    {
    author: "Kennedy Howell",
    genre: "thriller",
    id: 3,
    price: 68.61,
    title: "compressing Soft mint green",
    _id: "5d19cd74a0c3e2c7f68ff802"
    }
    ];

    it("renders component when there are no books", () => {
    const books = [];
    const wrapper = shallow(
        <BookList books={books} handleEditClick={() => {}} />
    );
    expect(wrapper).toMatchSnapshot();
    });

    it("renders component when there are three books", () => {
    
    const wrapper = shallow(
        <BookList books={books3} handleEditClick={() => {}} />
    );
    expect(wrapper).toMatchSnapshot();
    });
});

Making changes and updating snapshots

Lets confirm this by opening up the BookList component and removing the : from after the genre. The test will now fail.

It will show precisely where the failure occurred. We can then either accept this change and update the snapshot or go fix the error.


Using render rather then shallow

shallow shows implementation details (i.e. the component names).

render shows the generated HTML.

Lets try it by changing one of the calls from shallow to render.

/src/components/BookList.test.js

it("renders component when there are no books", () => {
  const books = [];
  const wrapper = render(
    <BookList books={books} handleEditClick={() => {}} />
  );
  expect(wrapper).toMatchSnapshot();
});

When would we want to use render?


Testing price formatting

Add a test to check the price of the one digit book price: 53.7 is displayed as two decimal places.

/src/components/BookList.test.js

it("correctly formats the price to two decimal places", () => {
 
  const wrapper = render(
    <BookList books={[books3[0]]} handleEditClick={() => {}} />
  );
  // will need a test here
});

Let's look at the snap shot. src/components/__snapshots__/BookList.test.js.snap


Let's refactor to make this more testable.
We will add a span on the price.

/src/components/BookList.js

  render(){
      return (
        <div className="book-list">
        {this.props.books.map((item, index) => (
            <div onClick={() => this.handleClickBook(item)} key={index} className={item.genre}>
            {item.genre}: {item.title} by {item.author} (rrp <span className="price">${item.price}</span>) (id: {item.id})
            </div>
        ))}
      </div>
      );
    }

Now lets use enzyme to select the price element and compare the text.

/src/components/BookList.test.js

it("correctly formats the price to two decimal places", () => {
  const wrapper = mount(
    <BookList books={[books3[0]]} handleEditClick={() => {}} />
  );
    const text = wrapper.find(".price").text();

    expect(text).toEqual('$53.70');
});

The test will fail if we do not correctly format the price item.price.toFixed(2)

/src/components/BookList.js

  render(){
      return (
        <div className="book-list">
        {this.props.books.map((item, index) => (
            <div onClick={() => this.handleClickBook(item)} key={index} className={item.genre}>
            {item.genre}: {item.title} by {item.author} (rrp <span className="price">${item.price.toFixed(2)}</span>) (id: {item.id})
            </div>
        ))}
      </div>
      );
    }

Test to simulate click event

We will now test to see if the click to edit feature is wired up.

We will use enzyme to simulate a click and sinon to simulate a function. The code for the test:

/src/components/BookList.test.js

it("calls the edit method on click", () => {
  const spy = sinon.spy();
  const wrapper = mount(
    <BookList books={[books3[0]]} handleEditClick={() => {spy()}} />
  );
  
  console.log(spy);
  wrapper.find(".comedy").simulate("click");
  expect(spy.calledOnce).toBe(true);
});

Verify this is working by commenting out the method call in handleClickBook

/src/components/BookList.js

    handleClickBook = (book) => {
      console.log('clicked')
      // this.props.handleEditClick(book)
    }

The test should fail. Remove the comment and the test should all pass again.

Challenge

  1. Add more test to test the text of the BookList. You can test for the exact text being present.

  2. Complete rendering tests for the BookForm component using the code for BookForm.test.js below.

  3. Complete validation tests. This will require implementation of validation check in handleClick. When the Save button is clicked the form fields are validated. If they are not valid addNewBook is not called.

  4. Now that there are tests continue with the steps of the Form and own API challenge. See what refactoring when you have tests is like.

  5. Add testing to the Wish List app we built in an earlier challenge. Is there a way to refactor to make testing easier?

/src/components/BookForm.test.js

import React from "react";
import Enzyme, { shallow, render, mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { createSerializer } from "enzyme-to-json";
import sinon from "sinon";

import BookForm from "./BookForm";



describe('<BookForm /> rendering', () => {
    const books3 = [{
    author: "Ms. Remington Fahey",
    genre: "comedy",
    id: 1,
    price: 53.7,
    title: "Legacy Intelligent",
    _id: "5d19cd74a0c3e2c7f68ff800"
    },
    {
    author: "Isaac Steuber",
    genre: "thriller",
    id: 2,
    price: 20.99,
    title: "Incredible Incredible Plastic Computer",
    _id: "5d19cd74a0c3e2c7f68ff801"
    },
    {
    author: "Kennedy Howell",
    genre: "thriller",
    id: 3,
    price: 68.61,
    title: "compressing Soft mint green"
    }
    ];
    it("renders component when there is no book data", () => {
        const wrapper = render(
            <BookForm 
                addNewBook={() => {}} 
            />
        );
        expect(wrapper).toMatchSnapshot();
    });

    it("renders component when there is valid book data", () => {
        // add test
    });

    it("renders component when there is partial book data", () => {
        // add test
    });
});


describe('<BookForm /> validation', () => {
  it("Does call add book when there is valid book data", () => {
      const spy = sinon.spy();
      const wrapper = mount(
          <BookForm 
              addNewBook={() => {spy()}} 
              title={books3[0].title} 
              author={books3[0].author} 
              genre={books3[0].genre} 
              price={books3[0].price} 
            />
      );
          
      wrapper.find("button").first().simulate("click");
      expect(spy.calledOnce).toBe(true);
  });

  it("Does not call add book when there is a missing title", () => {
      // add test
  });

  it("Does not call add book when there is a missing author", () => {
      // add test
  });

  it("Does not call add book when there is a missing genre", () => {
      // add test
  });

  it("Does not call add book when there is a missing price", () => {
      // add test
  });

  it("Does not call add book when there is an invalid price", () => {
      // add test eg: 10e
  });

  it("Does not call add book when there is a negative price", () => {
      // add test 
  });
});
  
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment