Today we will be testing the Book app. Either use your version and add the BookList component or clone my version and code along.
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>
);
}
Change into the app folder
cd books-front-end
Install dependencies
npm install
Run the app
npm start
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
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"
}
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() });
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);
});
});
/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();
});
});
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.
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
?
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>
);
}
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.
-
Add more test to test the text of the BookList. You can test for the exact text being present.
-
Complete rendering tests for the
BookForm
component using the code forBookForm.test.js
below. -
Complete validation tests. This will require implementation of validation check in
handleClick
. When theSave
button is clicked the form fields are validated. If they are not validaddNewBook
is not called. -
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. -
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
});
});