Let's take React Tic-Tac-Toe example by someone named D Abramov. He must be somewhat important person, since this example was included in the React Tutorial. You can find my version of the game at bahmutov/react-tic-tac-toe-example.
The project uses react-scripts
to bundle and serve the application. The application itself only has 3 files in the src
folder
1 | repo/ |
The application file src/app.jsx
has a few components: Square, Board, Game and a function calculateWinner
.
E2E vs component tests
Usually, I consider end-to-end tests the most effective way to confirm the application works. The Cypress test interacts with the loaded application like a real user, and any error in its logic, code, bundling, or deployment is likely to be caught.
1 | describe('Tic-tac-toe', () => { |
The tests are nice, but what if we want to concentrate on the Square
component? Maybe we want to refactor it, maybe we want to see how it looks with different styles or props, there could be lots of reasons we want to really stress this component in isolation from the main application.
Component tests
By adding cypress-react-unit-test to the repo we can easily write component tests. Let's confirm the Square
component shows the value passed via a prop. For now we can simply export Square
from the app.jsx
and import it in the test file, and we can place the spec file alongside the component.
1 | export function Square(props) { |
1 | import React from 'react' |
We import the component and mount it, and then use Cypress commands to interact with the live component. Let's confirm it calls the passed prop on click. Ordinarily one would write a new unit test to separate the value test from the click test. But Cypress has built-in time traveling debugger, records movies on CI, takes screenshots on failures - you don't need to make component tests tiny just to have a good debugging experience. So I will continue expanding the same test.
1 | import React from 'react' |
The test passes.
Styles
The square component looks like a regular button, not like a board cell in the real game. This is because we only mounted the markup and never applied any styles to it. There are multiple options for styling components during tests, but the simplest is just to import the application's CSS from the spec file.
1 | import React from 'react' |
We can import './app.css'
because the specs are bundled using the same Webpack config as the application; if you can import a resource from the application code, you should be able to import it from the component test file.
Mocking imports
We have passed cy.stub to the component. This stub comes from Sinon.js included with Cypress. It works great when stubbing individual functions or a method on an object. But what about mocking ES6 module exports and imports?
The game component determines the winner by calling calculateWinner
function. The exact details are unimportant, but the code around it looks like this:
From the component test, we cannot reach and overwrite a private function calculateWinner
. But we could move it into an external module and import it.
1 | export function calculateWinner(squares) { |
1 | import {calculateWinner} from './utils' |
Now let's write a component test for the Game
component - and mock the ES6 module import.
1 | import React from 'react' |
The test passes - note how Winner: X
is immediately displayed, even before the first move is played 😀
Unit tests
We can write more end-to-end and component tests, using built-in code coverage as a guide. But what about the above function calculateWinner
? Do we only indirectly test it via component tests? No. We should also directly test it using unit tests.
1 | import { calculateWinner } from './utils' |
We exercise different data inputs rather than code paths to make sure the function works correctly. The tests do not have a GUI, but the Command Log still shows useful information.
If there is an error, the code frame immediately shows the problem. For example if we change the last assertion to )).to.equal(x)
we get the precise source code location
More info
For more reasons behind component testing, read My Vision for Component Tests blog posts, and visit the cypress-react-unit-test repo.