Split A Test.Bash 2022 Test

How to spit a single Cypress test into several smaller isolated tests.

In this blog post I will continue working with my solution to the Test.Bash 2022 UI Automation challenge. I have described and recorded a video of my solution in the blog post Solving Test.Bash 2022 UI Challenge With Cypress. In this blog post, I will try to split the single test into a few shorter isolated tests that show a few tricks I normally employ in my Cypress testing.

🎁 You can find the original test and the refactored tests in the repo bahmutov/test-bash-2022-ui.

The original test

The test as written for the challenge entered the message like a user, then logged into the system as an admin, then viewed the message. So the test did several things together:

cypress/e2e/spec.cy.js
1
2
3
4
5
it('sends a message successfully', { retries: 2 }, () => {
// enter the random message
// log in as the admin
// check the message
})

Our tests mostly accessed the elements using the data-testid attributes.

1
2
3
4
5
6
7
8
cy.get('.row.contact form').within(() => {
cy.get('[data-testid="ContactName"]').type(name)
cy.get('[data-testid="ContactEmail"]').type(email)
cy.get('[data-testid="ContactPhone"]').type(phone)
cy.get('[data-testid="ContactSubject"]').type(subject)
cy.get('[data-testid="ContactDescription"]').type(body)
cy.get('#submitContact').click()
})

The repetition of the data attributes is a little too verbose. Let's add a custom Cypress command. Since we are getting an element by its test id, I will call my custom command cy.got

cypress/e2e/split.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
Cypress.Commands.add('got', (testid) => {
return cy.get(`[data-testid="${testid}"]`)
})

cy.get('.row.contact form').within(() => {
cy.got('ContactName').type(name)
cy.got('ContactEmail').type(email)
cy.got('ContactPhone').type(phone)
cy.got('ContactSubject').type(subject)
cy.got('ContactDescription').type(description)
cy.get('#submitContact').click()
})

Ok, let the refactoring begin.

Split the test

Our single test is too long - the web application can reset during the test and the might fail. We got around it by using test retries, but let's make it better. Let's test each feature separately: submit a form, log in, view messages. To confirm the user can submit the form, we can enter the form, and observe the network call to ensure the backend accepts the form submission. We can use cy.intercept command to spy on the call.

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
33
it('sends a message successfully', () => {
const name = 'Test Cy User'
const email = '[email protected]'
const phone = '123-456-7890'
const subject = `Test Cy message ${Cypress._.random(1e5)}`
const description = Cypress._.repeat('message body ', 5)

cy.visit('/')

cy.intercept('POST', '/message/').as('message')
cy.get('.row.contact form').within(() => {
cy.got('ContactName').type(name)
cy.got('ContactEmail').type(email)
cy.got('ContactPhone').type(phone)
cy.got('ContactSubject').type(subject)
cy.got('ContactDescription').type(description)
cy.get('#submitContact').click()
})

const message = {
description,
email,
name,
phone,
subject,
}
cy.wait('@message').its('request.body').should('deep.equal', message)
cy.get('@message')
.its('response.body')
.should('deep.include', message)
.and('have.property', 'messageid')
.should('be.a', 'number')
})

Submit the form test

We have confirmed the submission using deep.equal assertion, and the response that includes the same form plus the messageid.

Log in

Now that the form is working, let's confirm the user can log in both using the UI and using the API (because that is what the UI does)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
it('can log in using UI', () => {
// Axios rejects a promise after checking auth route right away
cy.on('uncaught:exception', () => false)
cy.visit('/#/admin')
cy.got('username').type(Cypress.env('username'))
cy.got('password').type(Cypress.env('password'), {
log: false,
})
cy.got('submit').click()
cy.contains('a', 'Logout').should('be.visible')

cy.contains('.navbar', 'B&B Booking Management')
.should('be.visible')
.find('a[href="#/admin/messages"]')
.click()
cy.location('hash').should('equal', '#/admin/messages')
})

Log in using the page UI

Logging in using the UI works, so now let's test a faster way that other tests could use - logging in using the API call, just like the login page does. We can use the cy.request command.

1
2
3
4
5
6
7
8
it('can log in using API call', () => {
cy.request('POST', '/auth/login', {
username: Cypress.env('username'),
password: Cypress.env('password'),
})
cy.visit('/#/admin/messages')
cy.contains('a', 'Logout').should('be.visible').scrollIntoView()
})

Almost instant login via cy.request call

Tip: you can cache the browser session cookie to avoid logging in before each test by using cypress-v10-preserve-cookie, or cypress-data-session, or cy.session.

We can refactor the log in into its own custom command.

1
2
3
4
5
6
Cypress.Commands.add('login', () => {
cy.request('POST', '/auth/login', {
username: Cypress.env('username'),
password: Cypress.env('password'),
})
})

Then each test for the admin user can start with:

1
2
3
4
5
it('can log in using API call', () => {
cy.login()
cy.visit('/#/admin/messages')
cy.contains('a', 'Logout').should('be.visible').scrollIntoView()
})

View messages

Now that we can quickly log in, let's check if the admin user can see the posted messages. We will send a new message using a cy.request command - which is the same network call as the test "sends a message successfully" shows works.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
it('views the message', () => {
const name = 'Test Cy User'
const email = '[email protected]'
const phone = '123-456-7890'
const subject = `Test Cy message ${Cypress._.random(1e5)}`
const description = Cypress._.repeat('message body ', 5)
const message = {
description,
email,
name,
phone,
subject,
}
cy.request('POST', '/message/', message).its('status').should('equal', 201)
...
})

After the message has been posted successfully and returned status code 201, we can log in

1
2
3
4
5
...
cy.request('POST', '/message/', message).its('status').should('equal', 201)

cy.login()
cy.visit('/#/admin/messages')

The view message test logs in

Let's find our posted message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cy.visit('/#/admin/messages')

cy.get('.row.detail')
.should('have.length.greaterThan', 0)
.contains('.row', subject)
.should('have.class', 'read-false')
.click()
cy.contains('.message-modal', subject)
.should('be.visible')
.and('include.text', name)
.and('include.text', phone)
.and('include.text', email)
.and('include.text', subject)
.and('include.text', description)
.contains('button', 'Close')
.wait(1000) // just to make the video clear
.click()
cy.contains('.message-modal', subject).should('not.exist')
cy.contains('.row.detail', subject)
.should('have.class', 'read-true')
.and('not.have.class', 'read-false')

The full view message test

After this refactoring, each test is short and independent from each other. If we need to add more tests, we can use the existing tests as the starting point.

See also