Rest Easy Example

An example of using the cypress-rest-easy plugin.

If you never used cy.intercept command, you should. If you are using it a lot, maybe take a look at the new plugin cypress-rest-easy

cypress-rest-easy makes testing ... too easy

The example application

Imagine a TodoMVC web application with a REST data backend. Let's say we want to confirm that the app shows "No todos" component if there are no items to show.

1
2
3
<div class="no-todos" v-show="!loading && !todos.length">
<p>No todos yet! Add one above to get started.</p>
</div>

No todos message is shown

How would we write a Cypress test for this? It depends on how we handle the data. Our initial test could look like this

cypress/e2e/zero-todos.cy.js
1
2
3
4
5
6
7
describe('Todo app', () => {
it('should show no todos', () => {
cy.visit('/')
cy.get('.loaded')
cy.get('.no-todos').should('be.visible')
})
})

Passing test

But of course this test would fail if we had even a single todo item.

Failing test if there are items present

We need to control the data the application receives from the server.

Regular server

Let's start with the real server that we cannot control during testing. So we need to use the user interface to clear the data for example. Let's say we want to test how the app looks without any items. Since there might be data left by the previous users and tests, we need to write a conditional test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe('Todo app', () => {
it('should show no todos (UI control)', () => {
cy.visit('/')
cy.get('.loaded')
// if there are any todos, delete them one by one
cy.get('.todo-list li')
.should(Cypress._.noop)
.then(($todos) => {
if ($todos.length > 0) {
cy.wrap($todos, { log: false }).each(($todo, key) => {
cy.log(`Deleting todo ${key + 1} of ${$todos.length}`)
cy.get($todo).find('.destroy').invoke('show').click()
})
}
})
cy.get('.no-todos').should('be.visible')
})
})

Conditional UI test

Ughh, we need to start the server and use UI to delete the items, and the test is more complex than it needs to be. Tip: you can write conditional tests using my plugin cypress-if

Mocking the individual calls

If you are a fan of cy.intercept command and know how to use it well, you can rewrite the above test to be deterministic by mocking the GET /todos network call the web app uses to load the data from the server.

1
2
3
4
5
6
7
8
describe('Todo app', () => {
it('should show no todos (network control)', () => {
cy.intercept('GET', '/todos', [])
cy.visit('/')
cy.get('.loaded')
cy.get('.no-todos').should('be.visible')
})
})

Zero items returned by the network cy.intercept mock

Great, the test is much simpler and is deterministic. But if our test had to add an item, it would need to mock more network calls to avoid sending data to the real backend. Here is a test that loads zero items, adds a todo, then reloads the page. We have to set up separate GET /todos intercepts just to return different data sets for the first call (zero items) and the second call (1 item). And to avoid storing the created test item on the server, we need to mock the POST /todos call too.

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
34
35
describe('Todo app', () => {
it('adds one todo and persists it (network control)', () => {
cy.intercept(
{
method: 'GET',
url: '/todos',
times: 1,
},
[],
).as('getTodos1')
cy.visit('/')
cy.get('.loaded')
cy.get('.no-todos').should('be.visible')
cy.intercept('POST', '/todos', {}).as('postTodo')
cy.get('.new-todo').type('Buy milk{enter}')
cy.get('.todo-list li').should('have.length', 1)
// if we reload the page, the todo should be there
cy.intercept(
{
method: 'GET',
url: '/todos',
times: 1,
},
[
{
id: 1,
title: 'Buy milk',
completed: false,
},
],
).as('getTodos2')
cy.reload()
cy.get('.todo-list li').should('have.length', 1)
})
})

Add one item test

The test passes, but it was complicated and not necessarily correct; the item might have a different id property! To fully recreate the REST api, we would need to keep and store the real item sent by the web application on the POST /todos call.

cypress-rest-easy plugin

If you need a REST backend API created using cy.intercept mocks, use the plugin cypress-rest-easy and add the test config object with the "rest" object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'cypress-rest-easy'

describe('Todo app', () => {
it(
'adds one todo and persists it (rest-easy)',
{ rest: { todos: [] } },
() => {
cy.visit('/')
cy.get('.loaded')
cy.get('.no-todos').should('be.visible')
cy.get('.new-todo').type('Buy milk{enter}')
cy.get('.todo-list li').should('have.length', 1)
// if we reload the page, the todo should be there
cy.reload()
cy.get('.todo-list li').should('have.length', 1)
},
)
})

The full test using mock backend created using cypress-rest-easy plugin

The empty REST resource list is created using the config object { rest: { todos: [] } }. It starts with an empty list (you could start with real items or pass a JSON fixture name to load data). Each call to POST /todos updates the list. The second GET /todos call returns the real list of items. We can even access the list from the test, since the mock server is "running" in the browser memory. Let's confirm the data-todo-id attribute of the created list element, it should have the id value.

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
it(
'adds one todo and persists it (rest-easy)',
{ rest: { todos: [] } },
() => {
cy.visit('/')
cy.get('.loaded')
cy.get('.no-todos').should('be.visible')
cy.get('.new-todo').type('Buy milk{enter}')
cy.get('.todo-list li').should('have.length', 1)
// if we reload the page, the todo should be there
cy.reload()
cy.get('.todo-list li').should('have.length', 1)
// direct access to the todos list
cy.wrap(Cypress.env('todos'), { log: false })
.should('have.length', 1)
.its(0)
.then((item) => {
// confirm the item ID
expect(item.id, 'item ID').to.be.a('string')
cy.contains('li.todo', 'Buy milk').should(
'have.attr',
'data-todo-id',
item.id,
)
})
},
)

Check the item id attribute

Even the most complicated tests are easy to write when you use cypress-rest-easy. And you don't even need the real API backend, since all resource calls are mocked.

🎁 You can find the source code for this blog post in the repo bahmutov/todo-ai-example in the branch rest-easy-example.