Test Refactoring Example

A step-by-step tutorial showing a Cypress test refactoring.

Recently a user posted on LinkedIn and Twitter a Cypress test that checks each card element on the page against the data object. Here is the posted code screenshot.

The initial test

In this blog post I will show how to refactor the above test to make it simpler and more accurate. There are hidden problems in the test code that might make it pass accidentally. We want to avoid such false positives, since they destroy the confidence in the ability of our automated tests to discover problems.

🎁 You can find the source code in the recipe "Check Cards" hosted on my Cypress examples site. You can watch me refactoring the code and explaining how I go about it in the video Check Cards Test Refactoring 📺.

The initial code

Let's take the following HTML fragment to be our dummy "app".

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
<main>
<ul>
<li>
<h2>Project A</h2>
<div>10 issues</div>
<div>4 events in the last 24 hours</div>
<div>stable</div>
<a href="/dashboard/issues">issues</a>
</li>
<li>
<h2>Project B</h2>
<div>10 issues</div>
<div>1 event in the last 24 hours</div>
<div>error</div>
<a href="/dashboard/issues">issues</a>
</li>
<li>
<h2>Project C</h2>
<div>100 issues</div>
<div>10 events in the last 24 hours</div>
<div>critical</div>
<a href="/dashboard/issues">issues</a>
</li>
</ul>
</main>

We need the list of projects to be the source of truth. We will check the page against this array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const mockProjects = [
{
name: 'Project A',
numIssues: 10,
numEvents24h: 4,
status: 'stable',
},
{
name: 'Project B',
numIssues: 10,
numEvents24h: 4,
status: 'error',
},
{
name: 'Project C',
numIssues: 10,
numEvents24h: 10,
status: 'critical',
},
]

The initial code that closely resembles the code screenshot is below.

1
2
3
4
5
6
7
8
9
10
11
cy.get('main')
.find('li')
.each(($el, index) => {
cy.wrap($el).contains(mockProjects[index].name)
cy.wrap($el).contains(mockProjects[index].numIssues)
cy.wrap($el).contains(mockProjects[index].numEvents24h)
cy.wrap($el).contains(mockProjects[index].status)
cy.wrap($el)
.find('a')
.should('have.attr', 'href', '/dashboard/issues')
})

The test passes, all is green and good in the world.

The initial test passes

Refactor the code

Let's shorten the code. We don't have to use cy.wrap($el) multiple times. We can use the expect($el).to.include.text(...) assertion from Chai-jQuery library to verify the text is present.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cy.get('main')
.find('li')
.each(($el, index) => {
const { name, numIssues, numEvents24h, status } =
mockProjects[index]

expect($el)
.to.include.text(name)
.and.to.include.text(numIssues)
.and.to.include.text(numEvents24h)
.and.to.include.text(status)

cy.wrap($el).find('a[href="/dashboard/issues"]')
})

Since the page is static by the time we start checking, the expect(...).to... assertions work just fine. Alternatively, we can use cy.wrap($el).within(() => ...) commands to limit the search to the element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cy.get('main')
.find('li')
.each(($el, index) => {
const { name, numIssues, numEvents24h, status } =
mockProjects[index]

cy.wrap($el).within(() => {
cy.contains(name)
cy.contains(numIssues)
cy.contains(numEvents24h)
cy.contains(status)
cy.get('a[href="/dashboard/issues"]')
})
})

Using cy.within command

I really like using cy.within command. It shortens the code and makes it less likely to find a wrong element accidentally. Check out my video 📺 Find The Right Item Using The cy.within Command Or The Parent Selector for another cy.within example.

Use selectors

There is a problem with out test. If you have over individual contains commands in the left Command Log column, you can see the element if finds. Oops, seems we tried to find cy.contains('4') and accidentally found 1 even in the last 24 hours. Not what we expected to find.

The test passed when it should have failed

I really dislike using cy.contains(partial text) command, and instead prefer cy.contains(selector, partial text). Let's add data-cy attributes to our HTML markup to be able to precisely find elements.

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
<main>
<ul>
<li class="card">
<h2 data-cy="name">Project A</h2>
<div data-cy="issues">10 issues</div>
<div>
<span data-cy="24h">4</span> events in the last 24 hours
</div>
<div data-cy="status">stable</div>
<a href="/dashboard/issues">issues</a>
</li>
<li class="card">
<h2 data-cy="name">Project B</h2>
<div data-cy="issues">10 issues</div>
<div>
<span data-cy="24h">1</span> event in the last 24 hours
</div>
<div data-cy="status">error</div>
<a href="/dashboard/issues">issues</a>
</li>
<li class="card">
<h2 data-cy="name">Project C</h2>
<div data-cy="issues">100 issues</div>
<div>
<span data-cy="24h">10</span> events in the last 24 hours
</div>
<div data-cy="status">critical</div>
<a href="/dashboard/issues">issues</a>
</li>
</ul>
</main>

Now we can target the precise element when searching for each field.

1
2
3
4
5
6
7
8
9
10
11
12
13
cy.get('main li.card').each(($el, index) => {
cy.log(`checking card **${index + 1}**`)
const { name, numIssues, numEvents24h, status } =
mockProjects[index]

cy.wrap($el).within(() => {
cy.contains('[data-cy=name]', name)
cy.contains('[data-cy=issues]', `${numIssues} issues`)
cy.contains('[data-cy=24h]', numEvents24h)
cy.contains('[data-cy=status]', status)
cy.contains('a[href="/dashboard/issues"]', 'issues')
})
})

Super. We now don't find stray elements with the same text.

We target the right element when using cy.contains command

Use data as the source of truth

Our test still does it backwards. It looks at the page and checks each li item:

1
2
3
4
cy.get('main li.card').each(($el, index) => {
cy.log(`checking card **${index + 1}**`)
...
})

What if our page rendering is incorrect and it only renders the first card? No problem, the test passes!

The test can pass even if the page does not render 2 out of 3 cards

Remember: you cannot trust the page, but you can trust the data. Thus you should iterate over the data list of projects and check each item against what is on the page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// confirm the correct number of items is shown
cy.get('main li.card').should('have.length', mockProjects.length)
// iterate over the data to confirm the info
// for each card is shown correctly
mockProjects.forEach((project, index) => {
const { name, numIssues, numEvents24h, status } = project
cy.get('main li.card')
.eq(index)
.within(() => {
cy.contains('[data-cy=name]', name)
cy.contains('[data-cy=issues]', `${numIssues} issues`)
cy.contains('[data-cy=24h]', numEvents24h)
cy.contains('[data-cy=status]', status)
cy.contains('a[href="/dashboard/issues"]', 'issues')
})
})

The test should iterate over the data and check the rendered elements

You can get the data the page is derived from by observing network traffic using the cy.intercept command or by accessing the data in the application.