Creating a new fully featured React application using Create React App v3 is as easy as two NPM commands
$ npm i -g create-react-app
+ [email protected]
$ create-react-app my-new-app
Then switch to the new folder my-new-app
and run npm start
or yarn start
- the application will be transpiled, and bundled, and will be running locally at url localhost:3000
. Under the hood the start
script calls the react-scripts
commands to actually do the heavy lifting: invoking the babel
module to transpile the source code, running the webpack-dev-server
to serve the application locally, etc. The package.json
below shows the default configuration after scaffolding the project.
{
"scripts": {
"start": "react-scripts start"
},
"dependencies": {
"react-scripts": "2.1.5"
}
}
Note: this post discusses applications created using create-react-app
. If you need code coverage for applications that use Next.js see next-and-cypress-example.
Code Coverage
If you have watched the Complete Code Coverage with Cypress webinar (and you totally should, it was a great webinar), then you are probably wondering how to instrument your new shiny CRA-created application to produce a code coverage report after running Cypress end-to-end tests. It is difficult - because react-scripts
hides the Babel configuration from you!
You could eject react-scripts
and get all the underlying dev dependencies in the project exposed and amenable to changes. But this is unnecessary - we have thought about CRA users and have created a simple way to just instrument the served application source code without ejecting react-scripts
. You can find the helper NPM module @cypress/instrument-cra
at cypress-io/instrument-cra, and it should be a 2 line operation to get your code instrumented and ready to be tested.
First, install this module as a dev dependency
$ npm i -D @cypress/instrument-cra
+ @cypress/[email protected]
Second, require the module when running react-scripts start
command using -r
or its full alias --require
option.
{
"scripts": {
"start": "react-scripts -r @cypress/instrument-cra start"
}
}
That is it. You can check if the application source code has been instrumented by opening the DevTools after running npm start
and checking the window.__coverage__
object - it should have the data for your application source files served from the "src" folder:
Example application
Cory House has coryhouse/testing-react repository with a small SPA created with CRA v3 which makes for a perfect example of using code coverage to guide test writing. The repo has Cypress as a dev dependency already, so we can go straight to adding code instrumentation and reporting coverage.
First, we will instrument the code as shown above.
$ npm i -D @cypress/instrument-cra
+ @cypress/[email protected]
In package.json
add helper scripts to start the app, the mock API and open Cypress using start-server-and-test utility
{
"scripts": {
"start": "npm-run-all start-dev start-mockapi",
"start-dev": "react-scripts -r @cypress/instrument-cra start",
"start-mockapi": "json-server ...",
"cypress": "cypress open",
"dev": "start-test 3000 cypress"
}
}
To save collected code coverage, we can follow the setup steps from @cypress/code-coverage plugin.
First, install the Cypress code coverage plugin and its peer dependencies:
$ npm i @cypress/code-coverage nyc istanbul-lib-coverage
+ [email protected]
+ [email protected]
+ @cypress/[email protected]
Add to your cypress/support/index.js
file plugin's commands
import '@cypress/code-coverage/support'
Register tasks in your cypress/plugins/index.js
file
module.exports = (on, config) => {
on('task', require('@cypress/code-coverage/task'))
}
These tasks will be used by the plugin to send the collected coverage after each test and merged into a single report.
⚠️ Note: since this blog post came out, we have changed how @cypress/code-coverage
registers itself. Please look at the README in cypress-io/code-coverage.
First test
We don't have any end-to-end tests, so let's write our first test. The application asks the user to enter miles driven and price per gallon for two cars and calculates the fuel savings (or losses).
Let's write a Cypress test in the cypress/integration/spec.js
file. The test repeats everything a user is doing in the animation above.
/// <reference types="cypress" />
beforeEach(() => {
cy.visit('/')
})
it('saves fuel', () => {
cy.contains('Demo App').click()
cy.url().should('match', /fuel-savings$/)
cy.get('#newMpg').type('30')
cy.get('#tradeMpg').type('20')
cy.get('#newPpg').type('3')
cy.get('#tradePpg').type('3')
cy.get('#milesDriven').type('10000')
cy.get('#milesDrivenTimeframe').select('year')
cy.get('td.savings')
.should('have.length', 3)
.and('be.visible')
.first() // Monthly
.should('contain', '$41.67')
cy.get('#saveCompleted').should('not.be.visible')
cy.get('#save').click()
cy.get('#saveCompleted').should('be.visible')
})
The test runs and passes.
The code coverage report has been saved automatically after the tests have finished. Open it with $ open coverage/lcov-report/index.html
and see that a single test can reach a lot of code.
Stubbing XHR
Notice the failed XHR call from the application after cy.get('#save').click()
command. You can see the failed call in the DevTools Network tab.
The frontend is calling the backend API, which does not exist yet! Luckily, Cypress includes network stubbing that allows you to write full end-to-end tests before the external APIs even exist. Here is the updated portion of the test where we will intercept the POST /fuelSavings
XHR call and respond with an empty object and status 200.
// stub API call
cy.server()
// does not really matter what the API returns
cy.route('POST', 'http://localhost:3001/fuelSavings', {}).as('save')
cy.get('#saveCompleted').should('not.be.visible')
cy.get('#save').click()
cy.get('#saveCompleted').should('be.visible')
The application is now happy, because the mock "server" responds with 200 HTTP status. If we click on the XHR command, we can see the details of the request made by the application.
Let's make our test even tighter - let us assert the request body. This way we can ensure that our application always sends the expected data, even as we refactor front-end source code in the future. Since we already gave the XHR stub an alias name save
, we can simply get it in our test and assert its properties.
cy.get('#save').click()
cy.get('#saveCompleted').should('be.visible')
cy.wait('@save')
.its('request.body')
.should('deep.equal', {
milesDriven: 10000,
milesDrivenTimeframe: 'year',
newMpg: 30,
newPpg: 3,
tradeMpg: 20,
tradePpg: 3
})
Hmm, we have a slight problem. The application is sending a timestamp to the backend, which is dynamic and prevents us from using deep.equal
assertion. Luckily, we can control the clock in the application using cy.clock
method. Let's set a mock date before clicking "Save" - this way the timestamp sent to the server will always be the same.
// before we try saving, let's control the app's clock
// so it sends the timestamp we expect
// @see https://on.cypress.io/clock
const mockNow = new Date(2017, 3, 15, 12, 20, 45, 0)
cy.clock(mockNow.getTime())
cy.get('#save').click()
cy.get('#saveCompleted').should('be.visible')
// check the "save" POST - because we set the mock date
// in our application, we know exactly all the fields we expect
// the application to send to the backend
cy.wait('@save')
.its('request.body')
.should('deep.equal', {
dateModified: mockNow.toISOString(),
milesDriven: 10000,
milesDrivenTimeframe: 'year',
newMpg: 30,
newPpg: 3,
tradeMpg: 20,
tradePpg: 3
})
Increasing code coverage
After the first test, our code coverage stands at 77%. Let's look at a couple of missed areas and how we can increase the coverage. For example in file src/api/fuelSavingsApi.js
we have missed the error path.
Here is the test that reaches this line - it stubs the network call, responds with an error, and stubs the console.log
method in the application's window.
// stub API call - make it fail
cy.server()
cy.route({
method: 'POST',
url: 'http://localhost:3001/fuelSavings',
status: 500,
response: ''
})
cy.window().then(w => cy.stub(w.console, 'log').as('log'))
cy.get('#save').click()
cy.get('@log').should('have.been.calledOnce')
Once this test finishes, we can confirm that it hits the line we want to reach
Next we can see that we are missing tests that open the "About" page, and the 404 page has never been triggered.
Here are a couple of tests that exercise the additional pages:
/// <reference types="cypress" />
beforeEach(() => {
cy.visit('/')
})
it('has greeting and links', () => {
cy.contains('h1', 'Demo App').should('be.visible')
cy.contains('[aria-current=page]', 'Home')
.should('be.visible')
.and('have.attr', 'href', '/')
})
it('opens about page', () => {
cy.contains('h1', 'Demo App').should('be.visible')
cy.contains('About').click()
cy.get('#about-header').should('be.visible')
})
it('has not found page', () => {
cy.contains('h1', 'Demo App').should('be.visible')
cy.visit('/nosuchpage')
cy.contains('404').should('be.visible')
cy.contains('a', 'Go back').click()
cy.location('pathname').should('equal', '/')
})
Unit tests
Taken together, the application and page specs cover almost 80% of the code
The largest number of code not covered by the tests is in src/utils
files.
Let's look at the math.js
source for example.
Do you notice anything unusual? Functions addArray
and convertToPennies
are never called by the outside code! I guess they were superseded by the roundNumber
function that is used by the outside code. The code coverage report helps us find unused code and delete it - as long as the tests still pass, we are good.
The edge cases in roundNumber
are still necessary to cover with tests though. For example, it is hard to trigger the numberToRound === 0
condition from end-to-end tests. We need to write a unit test!
Cypress can run unit tests, and before we write any, we need to ensure that spec files are instrumented, just like our application code was instrumented to collect code coverage. Just add the line to handle `file:preprocessor` event to the cypress/plugins/index.js
file. The preprocessor included with Cypress code coverage plugin will instrument the loaded spec files and any code loaded from the spec files directly.
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// custom tasks for sending and reporting code coverage
on('task', require('@cypress/code-coverage/task'))
// this line instruments spec files and loaded unit test code
on(
'file:preprocessor',
require('@cypress/code-coverage/use-browserify-istanbul')
)
}
Now we can write a unit test, directly importing application sources into specs.
/// <reference types="cypress" />
import { roundNumber } from '../../src/utils/math'
context('unit tests', () => {
describe('roundNumber', () => {
it('correctly rounds different numbers', () => {
expect(roundNumber(0.1234, 2), '0.1234 to 2').to.equal(0.12)
expect(roundNumber(0), '0').to.equal(0)
expect(roundNumber(), 'empty string').to.equal('')
expect(roundNumber(0.1234), '0.1234').to.be.NaN
expect(roundNumber(0.1234, -1), '0.1234 to -1').to.equal(0)
})
})
})
The test passes, and notice how there is no application loaded on the right, since the unit test never calls cy.visit
But the code coverage report shows that we cover all statements and branches in the math.js
file
Similarly, we can chase all source lines in other utility files, writing a few more unit tests. You can find them in cypress/integration/utils-spec.js
file. The final coverage report is 96% - only missing some redux store configuration paths, which is fine.
You can find the entire pull request that adds code instrumentation, coverage reporting and tests in https://github.com/coryhouse/testing-react/pull/3