Two Simple Tricks To Make Your Cypress Tests Better

Two very simple ideas to immediately improve your Cypress tests.

TLDR;

  1. Use log command instead of code comments
  2. Use cy-spok instead of multiple individual, deep.include, and deep.equal assertions

Log, do not comment

This is a typical test where several logical sections are prefaced with code comments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// https://github.com/bahmutov/cypress-map
import 'cypress-map'
// https://www.chaijs.com/plugins/chai-sorted/
chai.use(require('chai-sorted'))

it('sorts the prices', () => {
cy.visit('/')
// enter user information
cy.get('[data-test="username"]').type('standard_user')
cy.get('[data-test="password"]').type('secret_sauce')
cy.get('[data-test="login-button"]').click()
// transition to the inventory page
cy.location('pathname').should('equal', '/inventory.html')
// sort by price
cy.get('.product_sort_container').select('lohi')
// get the list of prices
// convert to numbers
// and confirm they are sorted
cy.get('.inventory_item_price')
.map('innerText')
.mapInvoke('substr', 1)
.map(Number)
.print()
.should('be.ascending')
})

Let's say the test fails. How many items were there? Did it finish each section? In longer tests, it is hard to say without carefully watching the video of the entire test run. Here is a better way: move each comment into cy.log command. Make those logs bold using **...** Markdown syntax.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
it('sorts the prices', () => {
cy.visit('/')
cy.log('**enter user information**')
cy.get('[data-test="username"]').type('standard_user')
cy.get('[data-test="password"]').type('secret_sauce')
cy.get('[data-test="login-button"]').click()
cy.log('**transition to the inventory page**')
cy.location('pathname').should('equal', '/inventory.html')
cy.log('**sort by price**')
cy.get('.product_sort_container').select('lohi')
cy.log('**confirm the prices are sorted**')
cy.get('.inventory_item_price')
.map('innerText')
.mapInvoke('substr', 1)
.map(Number)
.print()
.should('be.ascending')
})

Each comment like // enter user information becomes a command cy.log('**enter user information**'). It still explains what the test is about to do, plus it shows up in your screenshots and videos.

Log commands explain what is going on

Here is how you can make it even better. Install cypress-log-to-term and include it from your Cypress config and spec files. Now you see all those logs in your terminal too.

Log the messages to the terminal

Log the current subject

The plugin cypress-log-to-term has one powerful feature. It overwrites cy.log command AND makes it better. For example, let's log the important information about the test and what it "sees" on the page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
it('sorts the prices', () => {
cy.visit('/')
cy.log('**enter user information**')
cy.get('[data-test="username"]').type('standard_user')
cy.get('[data-test="password"]').type('secret_sauce')
cy.get('[data-test="login-button"]').click()
cy.log('**transition to the inventory page**')
cy.location('pathname').should('equal', '/inventory.html').log('at %o')
cy.log('**sort items by price**')
cy.get('.product_sort_container').select('lohi')
cy.log('**confirm the prices are sorted**')
cy.get('.inventory_item_price')
.map('innerText')
.print('prices %o')
.mapInvoke('substr', 1)
.map(Number)
.print()
.should('be.ascending')
.log('prices low to high %o')
})

Notice the extra .log(...) commands attached:

1
2
3
4
5
6
cy.location('pathname').should('equal', '/inventory.html').log('at %o')
...
cy.get('.inventory_item_price')
...
.should('be.ascending')
.log('prices low to high %o')

I have extended the cy.log to take the formatting into account plus use the current subject, if any. Thus the terminal shows really useful information right away.

Use my cy.log command to print the current subject to the terminal

Use cy-spok

Imagine your application is making an Ajax request. You want to validate both the request and the response objects, plus the server response HTTP status code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
it('checks the calculate network call', () => {
cy.visit('/calculator.html')
cy.get('#num1').type('5')
cy.get('#num2').type('2')
cy.intercept('POST', '/calculate').as('calculate')
cy.get('#add').click()
cy.wait('@calculate')
.its('response.statusCode')
.should('equal', 200)
// need to get the intercept again to check the request
cy.get('@calculate')
.its('request')
.should((request) => {
expect(request.method, 'method').to.equal('POST')
expect(request.body, 'body').to.deep.equal({
a: 5,
b: 2,
operation: '+',
})
})
// if we want to check the response, need to get it again
cy.get('@calculate')
.its('response')
.should((response) => {
expect(response.body, 'body').to.deep.equal({
a: 5,
b: 2,
operation: '+',
answer: 7,
})
})
})

Checking multiple fields of the network intercept

Ughh, 20 lines of code just to validate the request and response in the single call. Even with this much code, the Command Log does not even show the objects and the actual values, since Chai truncates the assertion messages. Let's improve it using cy-spok.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// https://github.com/bahmutov/cy-spok
// install the plugin using "npm i -D cy-spok"
import spok from 'cy-spok'

it('checks the calculate network call (cy-spok)', () => {
cy.visit('/calculator.html')
cy.get('#num1').type('5')
cy.get('#num2').type('2')
cy.intercept('POST', '/calculate').as('calculate')
cy.get('#add').click()
cy.wait('@calculate').should(
spok({
request: {
method: 'POST',
body: {
a: 5,
b: 2,
operation: '+',
},
},
response: {
statusCode: 200,
body: {
a: 5,
b: 2,
operation: '+',
answer: 7,
},
},
}),
)
})

So simple, and the Command Log is beautiful

The command log created by cy-spok is beautiful

Using cy-spok we can confirm all the properties in a nested deep object, and we can confirm some of the properties, leaving others unchecked. We can also use predicate functions and built-in predicate checks. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cy.wait('@calculate').should(
spok({
request: {
method: (m) => m === 'GET' || m === 'POST',
body: {
a: 5,
b: spok.number,
operation: '+',
},
},
response: {
statusCode: spok.range(200, 204),
body: {
a: 5,
b: 2,
operation: spok.string,
answer: 7,
},
},
}),
)

The command log shows all relevant information about the above complex assertion

Validating the network call using cy-spok predicates

See also