Rest Easy

Instant REST backend mock using the cypress-rest-easy plugin.

In all these years of doing Cypress demos and examples, I used example web applications like this one that stored data on the backend. The front-end and the server communicate via REST api convention: GET /resource to get all items, GET /resource/:id to get a single item, DELETE /resource/id: to remove an item, etc.

Having a separate REST backend to serve the HTML front-end plus the API resources (I typically use json-server) creates challenges:

Cypress can serve static HTML files using the standard cy.visit command:

1
cy.visit('index.html')

But what about REST api backend? Can the cy.intercept command create a useful mock backend for us? Yes - if you use my new plugin cypress-rest-easy. Installation is easy:

1
$ npm i -D cypress-rest-easy

Then import the plugin from your E2E support file

cypress/support/e2e.js
1
import 'cypress-rest-easy'

The REST server is created from fixture files, each resource matches a single fixture file. For example, here is our example todos.json fixture file with 3 items

cypress/fixtures/todos.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
"title": "write code",
"completed": false,
"id": "7370875353"
},
{
"title": "random todo 54977",
"completed": false,
"id": "6254248866"
},
{
"title": "random todo 28869",
"completed": false,
"id": "567"
}
]

Let's define the mock server for a suite of tests. The declaration goes into a describe or it configuration object.

cypress/e2e/todos.cy.js
1
2
3
4
5
6
7
8
9
10
describe('Todos', { rest: { todos: 'todos.json' } }, () => {
beforeEach(() => {
cy.visit('app/index.html')
})

it('has 3 todos', () => {
cy.wait('@getTodos')
cy.get('li.todo').should('have.length', 3)
})
})

Notice how we are visiting a static HTML file using app/index.html file path, then confirming that 3 items are present on the page. The mock backend is defined using the declaration { rest: { todos: 'todos.json' } }. If you wanted several resources, just add them to the rest config object: { rest: { users: 'users.json', posts: 'posts.json' } }

E2E test running without a backend

List as data

Instead of a fixture filename, you can provide the data directly

1
2
3
4
5
6
7
8
9
10
11
{
rest: {
todos: [
{
id: '101-102-103',
title: 'First item',
completed: true,
},
],
},
}

Using an array to mock the data

Important: the REST mock uses a copy of the list for each test, thus the changes in one test should not affect the data seen in another test.

Automatic intercepts

You can see the automatic intercept routes created by the plugin by expanding the Routes section.

Rest APIs defined for the "todos" resource

The endpoints get automatic alias names, so you can refer to them from the test itself. Let's add new todo items and confirm the web application is making the expected POST /todos network call to add an item.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
describe('Todos', { rest: { todos: 'todos.json' } }, () => {
beforeEach(() => {
cy.visit('app/index.html')
})

it('can add a todo', () => {
cy.get('input.new-todo').type('Buy milk{enter}')
// verify the REST response
cy.wait('@postTodos').its('response.statusCode').should('eq', 201)
cy.get('@postTodos')
.its('response.body')
.should('deep.include', {
title: 'Buy milk',
completed: false,
})
// the front-end assigns the "id"
.and('have.property', 'id')
.should('be.a', 'string')
cy.get('li.todo').should('have.length', 4)
})
})

The above test uses GET and POST intercepts for the "todos" resource

Great! What else can we do?

We can point the mock API are a different base URL and let the mock server auto generate id property for each new posted item:

1
2
3
4
5
6
7
{
rest: {
baseUrl: '/api/v1',
id: true,
todos: 'todos.json'
}
}

Mock backend running at the "/api/v1" endpoint

Finally, we have direct access to the backend data, since it resides in the browser memory. For each resource, the array is automatically stored in the Cypress.env(<name>) object and is accessible from the test itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
describe('Todos', { rest: { todos: 'todos.json' } }, () => {
beforeEach(() => {
cy.visit('app/index.html')
})

it('supports GET /:id', () => {
const todos = Cypress.env('todos')
const firstItemId = todos[0].id
expect(firstItemId, 'first item id').to.be.a('string')
// perform the request using the same fetch API
// as the application does which cy.intercept() sees
cy.window()
.invokeOnce('fetch', `/todos/${firstItemId}`)
.as('firstItem')
.its('status', { timeout: 0 })
.should('eq', 200)

cy.log('response body')
cy.get('@firstItem')
.invokeOnce('json')
.should('deep.equal', todos[0])
})
})

The reference const todos = Cypress.env('todos') stays valid through the test.

The test accesses the list of items directly

Finally, all intercepts are reset automatically before each test, so there is no cleanup necessary.

Just write your end-to-end tests, the rest is easy!

See also

Watch the video cypress-rest-easy Plugin Introduction below: