Cypress: Overview and best practices
By Jonathan Machado
January 1, 2021
This week I'm diving deep into Cypress, its patterns and best practices!
Cypress has native access to everything
Because Cypress operates within your application, that means it has native access to every single object. Whether it is the window, the document, a DOM element, your application instance, a function, a timer, a service worker, or anything else - you have access to it in Cypress. - Cypress How it works
This allows us to access the store or even a method from a library, like in the example below, where I'm using Nuxt Auth setToken
method to programmatically login:
Cypress.Commands.add('login', () => {
const email = Cypress.env('email')
const passwordHash = hashPassword(email, Cypress.env('password'))
cy.request({
method: 'POST',
url: 'http://localhost:5000/auth',
body: { email, password: passwordHash },
}).then((resp) => {
// ๐ using Nuxt Auth 'setToken' method to programmatically login
cy.window().then(({ app }) => {
app.$auth.setToken(resp.headers.token)
})
})
})
Do not use cy.wait(time)
This one is commonly mentioned on Cypress videos and blogs. Here are the main problems that can result from using cy.wait(time)
:
- It makes your suite too slow
- Your tests are still flaky
- It masks real application bugs
A good practice instead is to wait for a particular event to occur. We can do that by using the Cypress intercept()
command. We can use it together with cy.wait()
to wait for the request/response cycle to complete:
// Create a route interception aliased as 'getBooks'
cy.intercept('/books', []).as('getBooks')
// find the search input and type...
cy.get('#search').type('Design Patterns')
// once a request to /books responds, this 'cy.wait' will resolve
cy.wait('@getBooks')
// OR
// we can use the object yielded by 'cy.wait' to assert the response
cy.wait('@getBooks').its('response.statusCode').should('eq', 200)
Here's one video where this concept is covered:
Additional details about how to manage flaky tests and some important nuances about Cypress are also covered on this video.
Unprepared Elements issue
Another highlight from the video is a common Cypress scenario referred to as Unprepared Element. That's when an element in the DOM is not ready yet to be interacted with by Cypress. This happens because Cypress checks a lot of things to determine an elementโs visibility.
You can refer to the docs to see How Cypress ensures elements are actionable. Here's some of the things checked by Cypress:
- Ensure the element is not hidden.
- Ensure the element is not disabled.
- Ensure the element is not animating.
- Ensure the element is not covered.
The solution is to disable interaction with the element (ex. a button) until it can be interacted with. This can be accomplished in a few ways:
- Don't render the element at all
- Render as disabled
- Cover with an overlay
Testing edge cases
We can stub requests and responses to simulate how our app should respond to errors from our server. This allow us to:
-
Test edge cases like 'empty views' by forcing your server to send empty responses.
-
Test how your application responds to errors on your server by modifying response status codes to be 500.
Here's an example from the official docs:
cy.intercept(
{
method: 'GET', // Route all GET requests
url: '/users/*', // that have a URL that matches '/users/*'
},
[] // and force the response to be: []
)
You will notice that you can also use fixtures (a fixed set of data located in a file that is used in your tests) together with cy.intercept()
!
Ins and outs of Cypress commands
Cypress commands do not return values, they yield subjects from one command to the next one in the chain. How do they do that? Commands are essentially promises.
This is a great example from the docs that help us to understand how commands are chained under the hood. Notice how each promise returns a value to the following promise:
it('changes the URL when "awesome" is clicked', () => {
// THIS IS NOT VALID CODE.
// THIS IS JUST FOR DEMONSTRATION.
return cy
.visit('/my/resource/path')
.then(() => {
return cy.get('.awesome-selector')
})
.then(($element) => {
// not analogous
return cy.click($element)
})
.then(() => {
return cy.url()
})
.then((url) => {
expect(url).to.eq('/my/resource/path#awesomeness')
})
})