Acceptance Test Driven Development with React/Redux — Part 5

Juntao Qiu
ITNEXT
Published in
5 min readMar 15, 2018

--

update 1: This article is part of a series, check out the full series: Part1, Part 2, Part 3, part 4 and part 5.

update 2: I have published a book named Build React Application with Acceptance Test driven development to cover more topic and practices about ATDD with React, please check it out!

Searching

Our third feature is that a user can search books by its name. This is very useful when the book list become very long (it’s hard for a user to find what he is looking for when content is more than one screen or one page)

“Close-up of a desk with an iMac and a smartphone displaying the time” by Sabri Tuzcu on Unsplash

Acceptance test

Similarly, we start by writing an acceptance test:

test('Show books which name contains keyword', async () => {
await page.goto(`${appUrlBase}/`)
const input = await page.waitForSelector('input.search')
page.type('input.search', 'design')
// await page.screenshot({path: 'search-for-design.png'});
await page.waitForSelector('.book .title')
const books = await page.evaluate(() => {
return [...document.querySelectorAll('.book .title')].map(el => el.innerText)
})
expect(books.length).toEqual(1)
expect(books[0]).toEqual('Domain-driven design')
})

We try to type keyword design into .search input box, and expect that only Domain-driven design shows up in the book list.

The simplest way to implement is just modifying the BookListContainer and add an inputto it:

render() {
return (
<div>
<input type="text" className="search" placeholder="Type to search" />
<BookList {...this.state}/>
</div>
)
}

And then define a handling method for the change event for the input component:

filterBook(e) {
this.setState({
term: e.target.value
})
axios.get(`http://localhost:8080/books?q=${e.target.value}`).then(res => {
this.setState({
books: res.data,
loading: false
})
}).catch(err => {
this.setState({
loading: false,
error: err
})
})
}

and bind it on input component:

<input type="text" className="search" placeholder="Type to search" onChange={this.filterBook}
value={this.state.term}/>

Note that we are using books?q=${e.target.value} as the URL to fetching data, that's a full-text searching API provided by json-server, you just need to send books?q=domain to the backend and it will return all the content that contains domain.

You can try it on the command line like this:

curl http://localhost:8080/books?q=domain

Now our tests green again. Let’s jump to next step of the Red-Green-Refactoring.

Refactoring

Obviously, the filterBook is almost same to the code in componentDidMount, we can extract a function fetchBooks to remove the duplication:

componentDidMount() {
this.fetchBooks()
}
fetchBooks() {
const {term} = this.state
axios.get(`http://localhost:8080/books?q=${term}`).then(res => {
this.setState({
books: res.data,
loading: false
})
}).catch(err => {
this.setState({
loading: false,
error: err
})
})
}
filterBook(e) {
this.setState({
term: e.target.value
}, this.fetchBooks)
}

Emm, better than before. And since fetchBooks are coupling network request and statechanging together, we can split them into define 2 functions:

updateBooks(res) {
this.setState({
books: res.data,
loading: false
})
}
updateError(err) {
this.setState({
loading: false,
error: err
})
}
fetchBooks() {
const {term} = this.state
axios.get(`http://localhost:8080/books?q=${term}`).then(this.updateBooks).catch(this.updateError)
}
filterBook(e) {
this.setState({
term: e.target.value
}, this.fetchBooks)
}

Now the code turns much clean and easy to read.

One step further

Let’s say, someone else may want to use the search box we just finished on his own page, how can we reuse it? Actually, it’s very hard because currently the search box is highly tightly with the rest code in BookListContainer, we need to extract into another component SearchBox:

import React from 'react'function SearchBox({term, onChange}) {
return (<input type="text" className="search" placeholder="Type to search" onChange={onChange}
value={term}/>)
}
export default SearchBox

After that extraction, the render method of BookListContainer turns:

render() {
return (
<div>
<SearchBox term={this.state.term} onChange={this.filterBook} />
<BookList {...this.state}/>
</div>
)
}

And for unit tests we can simply test it this way:

import React from 'react'
import {shallow} from 'enzyme'
import SearchBox from './SearchBox'
describe('SearchBox', () => {
it('Handle searching', () => {
const onChange = jest.fn()
const props = {
term: '',
onChange
}
const wrapper = shallow(<SearchBox {...props}/>)
expect(wrapper.find('input').length).toEqual(1)
wrapper.simulate('change', 'domain')

expect(onChange).toHaveBeenCalled()
expect(onChange).toHaveBeenCalledWith('domain')
})
})

Note that we are using jest.fn() to create a spy object that can record the trace of invocations. And we use simulate API provided by enzyme to simulate a change event with domain as it's payload. We can then expect that onChange method has been called with data domain.

Now we noticed that SearchBox is just a presentational component, we can move it to components folder:

src
├── App.css
├── App.js
├── components
│ ├── BookDetail
│ │ ├── index.js
│ │ └── index.test.js
│ ├── BookList
│ │ ├── index.css
│ │ ├── index.js
│ │ └── index.test.js
│ └── SearchBox
│ ├── index.js
│ └── index.test.js
├── containers
│ ├── BookDetailContainer.js
│ └── BookListContainer.js
├── e2e.test.js
├── index.css
├── index.js
└── setupTests.js

Some style updates

.search {
box-sizing: border-box;
width: 100%;
padding: 2px 4px;
height: 32px;
}

Now our user interface looks quite like a real application:

Furthermore, let’s restructure the container folder to make it consistency with the componentfolder:

src
├── App.css
├── App.js
├── components
│ ├── BookDetail
│ │ ├── index.js
│ │ └── index.test.js
│ ├── BookList
│ │ ├── index.css
│ │ ├── index.js
│ │ └── index.test.js
│ └── SearchBox
│ ├── index.css
│ ├── index.js
│ └── index.test.js
├── containers
│ ├── BookDetailContainer
│ │ └── index.js
│ └── BookListContainer
│ └── index.js
├── e2e.test.js
├── index.css
├── index.js
└── setupTests.js

We defined an index.js in each folder, then you can simply import it by the folder name just like

import BookListContainer from "./containers/BookListContainer/"

without that, you may see some duplication in the path like this:

import BookListContainer from "./containers/BookListContainer/BookListContainer"

Great, we have finished all the 3 features! Let’s take a look at what we’ve got here:

  • 3 presentational components (BookDetail, BookList, SearchBox) and their unit tests
  • 2 container components (BookDetailContainer, BookListContainer)
  • 3 acceptance tests to cover the most valuable path (list, detail, and searching)

--

--