Note: this blog post takes the twelve testing examples from 12 Recipes for testing React applications using Testing Library blog post by João Forja, where the same examples are tested using testing-library. This blog post uses cypress-react-unit-test + Cypress combination to test exactly the same scenarios.
Note 2: you can find these tests in the repo bahmutov/12-testing-recipes
Table of Contents
- Invokes given callback
- Changes current route
- Higher Order Component
- Component cleans up on unmount
- Depends on Context Provider
- Uses functions that depend on time
- Custom hooks
- Portal
- Focus is on correct element
- Order of elements
- Selected option
- Dynamic page titles
Invokes given callback
- We're testing that after some interaction the component calls a given callback.
- We give a mock function to the component under test and interact with it so that it calls the callback. Then we assert we called the function with the expected parameters. If relevant, we also check the number of times the function was called.
/// <reference types="cypress" />
import React from 'react'
import { mount } from 'cypress-react-unit-test'
describe('Invoke callback', () => {
function Button({ action }) {
return <button onClick={() => action()}>Call</button>
}
it('callback is called on button click', () => {
const callback = cy.stub()
mount(<Button action={callback} />)
cy.contains('button', /call/i)
.click()
.then(() => {
expect(callback).to.have.been.calledOnce
expect(callback).to.have.been.calledWithExactly()
})
})
})
The test runs and we can see the stub count of 1 in the Cypress' Command Log on the left.
The same test can be written differently avoiding .then
by saving the reference to the stub using an alias.
it('callback is called on button click using an alias', () => {
mount(<Button action={cy.stub().as('callback')} />)
cy.contains('button', /call/i).click()
cy.get('@callback')
.should('have.been.calledOnce')
.and('have.been.calledWithExactly')
})
Changes current route
- We're testing that the component redirects the user to an expected router with the expected query parameters after an interaction.
- We first create a routing environment similar to that in which we'll use the component. We set up that environment so we can capture the URL to which the component will redirect us. We interact with the component to cause the redirect. We then assert that we were redirected to the URL we expected.
/// <reference types="cypress" />
import React, { useState } from 'react'
import { MemoryRouter, Route, useHistory } from 'react-router-dom'
import { mount } from 'cypress-react-unit-test'
describe('Changes current route', () => {
it('On search redirects to new route', () => {
let location
mount(
<MemoryRouter initialEntries={['/']}>
<Route path="/">
<SearchBar />
</Route>
<Route
path="/*"
render={({ location: loc }) => {
location = loc
return null
}}
/>
</MemoryRouter>,
)
cy.get('input#query').type('react')
cy.get('input[type=submit]')
.click()
.then(() => {
expect(location.pathname).to.equal('/search-results')
const searchParams = new URLSearchParams(location.search)
expect(searchParams.has('query')).to.be.true
expect(searchParams.get('query')).to.equal('react')
})
})
})
function SearchBar() {
const history = useHistory()
const [query, setQuery] = useState('')
return (
<form
onSubmit={function redirectToResultsPage(e) {
debugger
e.preventDefault()
history.push(`/search-results?query=${query}`)
}}
>
<label htmlFor="query">search</label>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.currentTarget.value)}
id="query"
/>
<input type="submit" value="go" />
</form>
)
}
You can see where the test is typing and the button it is clicking
Higher Order Component
- We're testing that a HOC gives the props we expect to the wrapped component.
- We first create a mock component for the HOC to wrap. The mock component will store the received props in a variable. After rendering the component returned by the HOC we assert that it gave the mock component the props we expected.
import React from 'react'
import { mount } from 'cypress-react-unit-test'
function withSum(WrappedComponent, numbersToSum) {
const sum = numbersToSum.reduce((a, b) => a + b, 0)
return () => <WrappedComponent sum={sum} />
}
describe('Higher Order Component', () => {
it('Adds number and gives result as a prop', () => {
let result
function WrappedComponent({ sum }) {
result = sum
return null
}
const ComponentWithSum = withSum(WrappedComponent, [4, 6])
mount(<ComponentWithSum />)
// mount is an asynchronous command
cy.then(() => {
expect(result).to.equal(10)
})
})
})
Seeing "Unknown" due to anonymous function in the mounting message weird, we can give it a label:
mount(<ComponentWithSum />, { alias: 'ComponentWithSum' })
Component cleans up on unmount
- We want to assert that a component subscribes after mount and unsubscribes after unmount.
- We start by mocking the subscription methods so we can assert they get called. We then render the component and assert that it subscribed. All that's left to do is make the component unmount and assert it unsubscribed.
/// <reference types="cypress" />
import React, { useEffect } from 'react'
import { mount, unmount } from 'cypress-react-unit-test'
function ComponentThatSubscribes({ subscriptionService }) {
useEffect(() => {
subscriptionService.subscribe()
return () => subscriptionService.unsubscribe()
}, [subscriptionService])
return null
}
describe('Component cleans up on unmount', () => {
it('Subscribes and unsubscribes when appropriate', () => {
const subscriptionService = {
subscribe: cy.stub().as('subscribe'),
unsubscribe: cy.stub().as('unsubscribe'),
}
mount(<ComponentThatSubscribes subscriptionService={subscriptionService} />)
cy.get('@subscribe')
.should('have.been.calledOnce')
.and('have.been.calledWithExactly')
unmount()
cy.get('@unsubscribe')
.should('have.been.calledOnce')
.and('have.been.calledWithExactly')
})
})
Again, both cy.stub
instances can be found in the Command Log.
Depends on Context Provider
- We want to test a component that depends on a context Provider
- To test the component, we'll recreate the environment in which we'll use the component. In other words, we'll wrap the component in the Context Provider.
/// <reference types="cypress" />
import React, { useContext } from 'react'
import { mount } from 'cypress-react-unit-test'
const UserContext = React.createContext()
function UserFullName() {
const { user } = useContext(UserContext)
return <p>{user.fullName}</p>
}
describe('Proivder', () => {
it('displays name of current user', () => {
mount(
<UserContext.Provider value={{ user: { fullName: 'Giorno Giovanna' } }}>
<UserFullName />
</UserContext.Provider>,
)
cy.contains('Giorno Giovanna').should('be.visible')
})
})
The test passes, and if we hover over the "contains" command, the found element is highlighted on the page.
Uses functions that depend on time
- We want to test a component that depends on real-time. In this example, that dependency comes from using setTimeout().
- When testing components that depend on real-time, we need to be aware that those tests shouldn't take too long.
One way to do that is to have the component receive the time interval as a prop to allow us to configure a shorter time interval for tests than we would have in production.One way to do so that is to mock the clock during the test to speed things up.
/// <reference types="cypress" />
import React, { useState, useEffect } from 'react'
import { mount } from 'cypress-react-unit-test'
function TrafficLight() {
const timeUntilChange = 500
const [light, setLight] = useState('Red')
useEffect(() => {
setTimeout(() => setLight('Green'), timeUntilChange)
}, [timeUntilChange])
return <p>{light}</p>
}
describe('Functions that depend on time', () => {
it('Changes from red to green to after timeout', () => {
cy.clock()
mount(<TrafficLight />)
cy.contains(/red/i).should('be.visible')
cy.tick(500)
cy.contains(/green/i).should('be.visible')
})
})
We are using cy.clock to freeze the application's clock. After checking if the light is red at the beginning, we fast-forward the timer by 500ms and check the light again. This time it shows "Green". We can confirm the DOM structure during each step by using the built-in Cypress time-traveling debugger.
Custom hooks
- We want to test a custom hook.
- Since we're testing a hook, we'll need to call it inside a component otherwise we'll get an error. Therefore, we'll create a mock component, use the hook inside it, and store what the hook returns in a variable. Now we can assert what we need to assert using that variable.
/// <reference types="cypress" />
import React, { useState, useCallback } from 'react'
import { mount } from 'cypress-react-unit-test'
function useCounter() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount((x) => x + 1), [])
return { count, increment }
}
describe('Hooks', () => {
it('counter increments', () => {
let counter
function MockComponent() {
counter = useCounter()
return null
}
mount(<MockComponent />)
.then(() => {
expect(counter.count).to.equal(0)
counter.increment()
})
.then(() => {
expect(counter.count).to.equal(1)
})
})
})
You can also test the hook directly - cypress-react-unit-test
will create a mock component for you.
import { useState, useCallback } from 'react'
import { mountHook } from 'cypress-react-unit-test'
function useCounter() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount(x => x + 1), [])
return { count, increment }
}
describe('useCounter hook', function() {
it('increments the count', function() {
mountHook(() => useCounter()).then(result => {
expect(result.current.count).to.equal(0)
result.current.increment()
expect(result.current.count).to.equal(1)
result.current.increment()
expect(result.current.count).to.equal(2)
})
})
})
Portal
- We want to test a component that's a portal.
- A portal needs a DOM node to be rendered into. So to test it, we'll have to create that DOM node. After we make the assertions, we'll have to remove the DOM node as not to affect other tests.
/// <reference types="cypress" />
import React, { useRef, useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
import { mount } from 'cypress-react-unit-test'
function PortalCounter() {
const el = useRef(document.createElement('div'))
const [count, setCount] = useState(0)
useEffect(() => {
const modalRoot = document.getElementById('modal-root')
const currentEl = el.current
modalRoot.appendChild(currentEl)
return () => modalRoot.removeChild(currentEl)
}, [])
return ReactDOM.createPortal(
<>
<section aria-live="polite">
count: <span data-testid="counter">{count}</span>
</section>
<button type="button" onClick={() => setCount((c) => c + 1)}>
inc
</button>
</>,
el.current,
)
}
describe('Portal', () => {
it('PortalCounter starts at 0 and increments', () => {
const modalRoot = document.createElement('div')
modalRoot.setAttribute('id', 'modal-root')
document.body.appendChild(modalRoot)
mount(<PortalCounter />)
cy.contains('[data-testid=counter]', 0)
cy.contains('button[type=button]', 'inc').click()
cy.contains('[data-testid=counter]', 1)
})
})
Focus is on correct element
- We want to test that the focus on the element we expect.
- We can verify if an element has focus or not by using
.should('have.focus')
assertion.
/// <reference types="cypress" />
import React from 'react'
import { mount } from 'cypress-react-unit-test'
function NameForm() {
return (
<form>
<label htmlFor="name">Name</label>
<input id="name" type="text" />
</form>
)
}
describe('Focus is on correct element', () => {
it('clicking on label gives focus to name input', () => {
mount(<NameForm />)
cy.contains('label', 'Name').click()
cy.get('input#name').should('have.focus')
})
})
Order of elements
- We want to test that a list of elements is rendered in the expected order.
- We'll take advantage of cy.get queries returning elements in the order in which they appear on the HTML.
- It's important to note that this approach doesn't take into account CSS that might change the order in which the elements are displayed.
/// <reference types="cypress" />
import React from 'react'
import { mount } from 'cypress-react-unit-test'
function NamesList({ names }) {
return (
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
)
}
describe('Order of elements', () => {
it('renders names in given order', () => {
const names = ['Bucciarati', 'Abbacchio', 'Narancia']
mount(<NamesList names={names} />)
cy.get('li').should(($li) => {
expect($li[0]).to.have.text(names[0])
expect($li[1]).to.have.text(names[1])
expect($li[2]).to.have.text(names[2])
})
})
})
In Cypress, you can see the component renders the elements in the expected order and then use visual testing to automatically validate it.
You can further simplify the above test using .each command
cy.get('li').each((li, k) => {
expect(li).to.have.text(names[k])
})
Selected Option
- We want to test that an input is checked.
- We can use
should('be.checked')
to test if an element is checked.
/// <reference types="cypress" />
import React from 'react'
import { mount } from 'cypress-react-unit-test'
function SeasonsForm() {
return (
<form>
<p>Best season:</p>
<section>
<input name="season" type="radio" id="winter" value="winter" />
<label htmlFor="winter">Winter</label>
<input name="season" type="radio" id="spring" value="spring" />
<label htmlFor="spring">Spring</label>
<input
name="season"
checked
readOnly
type="radio"
id="summer"
value="summer"
/>
<label htmlFor="summer">Summer</label>
<input name="season" type="radio" id="autumn" value="autumn" />
<label htmlFor="autumn">Autumn</label>
</section>
</form>
)
}
describe('Selected option', () => {
it('Has Summer pre-selected', () => {
mount(<SeasonsForm />)
cy.get('input[type=radio]#summer').should('be.checked')
})
})
Dynamic page titles
- We want to test that the title of the current page is updated.
- We access the current title by using cy.title. Even if the document title won't be immediately updated, Cypress will automatically waiting and retrying the assertion
should('equal', '1')
.
/// <reference types="cypress" />
import React, { useState } from 'react'
import { Helmet } from 'react-helmet'
import { mount } from 'cypress-react-unit-test'
function DocTitleCounter() {
const [counter, setCounter] = useState(0)
return (
<>
<Helmet>
<title>{String(counter)}</title>
</Helmet>
<button onClick={() => setCounter((c) => c + 1)}>inc</button>
</>
)
}
describe('Document title', () => {
it('Increments document title', () => {
mount(<DocTitleCounter />)
cy.title().should('equal', '0')
cy.contains('button', /inc/i).click()
cy.title().should('equal', '1')
})
})
Other resources
- The original blog post 12 Recipes for testing React applications using Testing Library where the above 12 examples came from
- More cypress-react-unit-test examples
- My Vision for Component Tests in Cypress
Top comments (1)
I really enjoyed seeing how the examples mapped from Jest+RTL to Cypress. I haven't yet written unit tests using Cypress, but I'll give it a try since I like the idea of having all of my tests run a real browser.
Also, thanks for referencing the original blog post!