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!

Package management

Let’s get started with some simple package installation and configuration first. Make sure you have node.js installed locally, after that, you can use npm to install the tools we need for build our Bookish application:

npm install yarn create-react-app --global

create-react-app

After the installation, we can use create-react-app provided by Facebook to create our project:

create-react-app bookish-react

create-react-app will help us install react , react-dom and a command line tool named react-scripts by default. And it will download those libraries and their dependencies automatically, such as webpack , babel , and so on. By using create-react-app we basically can do zero-config to make the application up and running.

After the creation, as the console log suggested, we need to jump into bookish-react folder, and run yarn start :

cd bookish-react

yarn start

And there will be a new browser tab opened automatically with address fulfilled http://localhost:3000 , the UI should look like this：

Project Structure

We don’t need all of the files generated by create-react-app , let's do some clean up first. We can remove all the irrelative files in src folder like this:

src

├── App.css

├── App.js

├── index.css

└── index.js

And modify the file content as following:

import React, { Component } from 'react';

-import logo from './logo.svg';

import './App.css';



class App extends Component {

render() {

return (

<div className="App">

- <header className="App-header">

- <img src={logo} className="App-logo" alt="logo" />

- <h1 className="App-title">Welcome to React</h1>

- </header>

- <p className="App-intro">

- To get started, edit <code>src/App.js</code> and save to reload.

- </p>

+ <h1>Hello world</h1>

</div>

);

} import React from 'react';

import ReactDOM from 'react-dom';

import './index.css';

import App from './App';

-import registerServiceWorker from './registerServiceWorker';



-ReactDOM.render(<App />, document.getElementById('root'));

-registerServiceWorker();

+ReactDOM.render(<App />, document.getElementById('root'));

Clean up the CSS file content as well:

- .App {

- text-align: center;

- }

-

-.App-logo {

- animation: App-logo-spin infinite 20s linear;

- height: 80px;

-}

-

-.App-header {

- background-color: #222;

- height: 150px;

- padding: 20px;

- color: white;

-}

-

-.App-title {

- font-size: 1.5em;

-}

-

-.App-intro {

- font-size: large;

-}

-

-@keyframes App-logo-spin {

- from { transform: rotate(0deg); }

- to { transform: rotate(360deg); }

-}

Then we got the UI like this:

Puppeteer

After that, we need to setup the acceptance tests environment first. In this book, we'll use Puppeteer to do the UI tests. Puppeteer from Google is built on top of Headless Chrome, it provides a lot of API to do the DOM manipulation, JavaScript evaluation, which can be used for run the UI tests.

First, we need to install it locally:

yarn add puppeteer --dev

with --dev option means we just add it as a dependency for development stage, we don't want to include it in production code.

Our first end 2 end test

The most difficult thing of TDD might be where to start, how we write the very first test?

Our first test could be:

Make sure there is a Heading element on the page, the content is Bookish

This test looks useless at the first glance, but actually, it can make sure that:

Frontend code can compile and translate

The browser can render our page correctly(without any script errors)

So, first we create a file e2e.test.js with the following content:

import puppeteer from 'puppeteer' const appUrlBase = 'http://localhost:3000' let browser

let page beforeAll(async () => {

browser = await puppeteer.launch({})

page = await browser.newPage()

}) describe('Bookish', () => {

test('Heading', async () => {

await page.goto(`${appUrlBase}/`)

await page.waitForSelector('h1')

const result = await page.evaluate(() => {

return document.querySelector('h1').innerText

}) expect(result).toEqual('Bookish')

})

}) afterAll(() => {

browser.close()

})

I know it looks a little bit scary, but actually it really simple. Let’s review it block by block.

import puppeteer from 'puppeteer'

Firstly, we import puppeteer , and then specify the devServer address, so Puppeteer knows whether to access the application

const appUrlBase = 'http://localhost:3000'

in beforeAll hook, we start the Chrome in headless mode:

beforeAll(async () => {

browser = await puppeteer.launch({})

page = await browser.newPage()

})

and then stop it in afterAll hook.

afterAll(() => {

browser.close()

})

async & await

Note here we’re marking the anymous function in beforeAll as async and putting an await keyword before puppeteer.launch() .

The async and await are ES6 syntax that can make the asynchorize programming much easier in JavaScript. Let's take a look at a simple example:

function fetchContactById(id) {

return fetch(`http://localhost:8080/contacts/${id}`).then((response) => {

return response.json()

})

}

In function fetchContactById , we use fetch to send http request and return the json response back (as a Promise object). Then we can consume this Promise object just by:

function main() {

fetchContactById(1).then((contact) => {

console.log(contact.name)

})

}

But by using async/await , we can simply define a variable to wait for the return of the function call -- the process is blocked -- and once the underlying Promise is resolved, the contact has the correct value and the control process is back and console.log is evaluated.

async function main() {

const contact = await fetchContactById(1)

console.log(contact.name)

}

There is no much differences you may say, but if we enhance the example above a little bit, say, by adding another API call fetchUserById and we need the returned value of that API to send the fetchContactById :

function fetchUserById(id) {

return fetch(`http://localhost:8080/users/${id}`).then((response) => {

return response.json()

})

}

Then the code becames like:

function main() {

fetchUserById('juntao').then((user) => {

fetchContactById(user.id).then((contact) => {

console.log(contact.name)

})

})

}

the indention could be keep growing and growing if there are requests send one by one, but by using the await/async pair, the code will be very clear like:

async function amain() {

const user = await fetchUserById('juntao')

const contact = await fetchContactById(user.id)

console.log(contact.name)

}

That’s much more compact and neat!

Then let’s take a look at the main block:

describe('Bookish', () => {

test('Heading', async () => {

await page.goto(`${appUrlBase}/`)

await page.waitForSelector('h1')

const result = await page.evaluate(() => {

return document.querySelector('h1').innerText

}) expect(result).toEqual('Bookish')

})

})

In the test case, we access port 3000 by using puppeteer , and wait for h1 to show up, and then invoke evaluate to call the native DOM script:

document.querySelector('h1').innerText

once we get the content of h1 , we can do a assertion:

expect(result).toEqual('Bookish')

Since create-react-app has already build jest testing framework in, we can simply run the following command to run the tests:

yarn test

Of course, we don’t have any implementation yet, the test failed:

Still, remember the red-green-refactor cycle? Since the test now failed (in red), we should make it pass first. We can simply modify the content in h1 to Bookish :

class App extends Component {

render() {

return (

<div className="App">

<h1>Bookish</h1>

</div>

);

}

}

Great, the test now passes. Go on the cycle, emm, refactor. But for now, it seems ok so we can skip this.

Commit code to CVS

Ok, we now have an acceptance test and its’ implementation, we can put the code to version control in case we need to look back in the future.

git init

Commit it locally

git add .

git commit -m "make the first e2e test pass"

Now look what we got here:

A running acceptance test

A page can render Bookish as a heading

Great, we can get started on the real requirement implementation.