Testing Edge Data Cases with Network Stubbing and App Actions

March 3, 2020

By Gleb Bahmutov

Your application might be a layered cake of historical data. Often old records are missing pieces because at first the web application never asked for them, or never validated them. The current web application might be much stricter with its user inputs, never allowing incomplete data to be entered.

Take a TodoMVC application. It might check the input text, disallowing empty titles.

// application code
addTodo(e) {
  // do not allow adding empty todos
  if (!e.target.value.trim()) {
    throw new Error('Cannot add a blank todo')
  }
  e.target.value = ''
  this.$store.dispatch('addTodo')
  this.$store.dispatch('clearNewTodo')
}

Note: you can find the application source code and example tests described in this blog post in the testing-workshop-cypress repository.

How will the web application handle Todos that somehow do have title: " "? Will the web application handle this data edge case gracefully? Or will the application lock up? Or will it most likely survive and show the list in some weird way?

Confirm empty titles cannot be entered

First, let's confirm that web application UI do not allow entered just blank space characters as a title. In the above code we see that the application is supposed to throw an exception - this behavior is not perfect, but we can test it.

/// <reference types="cypress" />
it('does not allow entered empty title', () => {
  cy.visit('/')
  cy.get('input.new-todo').type(' {enter}')
})
A test that tries to enter empty title

The test fails - because our application is throwing an error, which causes Cypress test to fail by default

Any exception fails the test

We expect the application to throw an error, thus we set an error event listener, following the documentation page linked from the error message https://on.cypress.io/uncaught-exception-from-application.

it('does not allow entered empty title', () => {
  cy.visit('/') // loads 2 todos
  cy.on('uncaught:exception', e => {
    // only ignore the error message from blank title
    return !e.message.includes('Cannot add a blank todo')
  })
  cy.get('input.new-todo').type(' {enter}')
  // confirm the blank todo has not been added
  cy.get('li.todo').should('have.length', 2)
  cy.get('li.todo label').should('not.have.text', ' ')
})
The test passes because the application does not allow empty titles

The test passes - but we need to be careful - if the application does NOT throw an error at all, our exception handler will never be reached. Properly accounting for the expected number of assertions would side-track the discussion at hand, I suggest you read the blog post When Can the Test Stop? for additional details.

Stubbing network calls

Let's test this. First, we need to know how our application gets the initial list of todos. The DevTools Network panel shows a call to GET /todos the application performs on load. This is how the front-end is loading the initial list of items from the database.

Initial GET /todos loads the data

The response object is an array of objects with title, id and completed properties.

The response list

Click the "Response" tab to see the original JSON text that you can copy.

Select the entire response and Copy to to the clipboard

Save the above text as the file cypress/fixtures/empty-title.json

[
  {
    "title": "write code",
    "completed": false,
    "id": "8400230918"
  },
  {
    "title": " ",
    "completed": false,
    "id": "4513882152"
  }
]

Now let's write a test - except instead of going to the backend, the initial GET /todos request will be intercepted by Cypress, and the fixture data will be returned using the cy.route command.

/// <reference types="cypress" />
it('renders empty title', () => {
  // prepare to stub network request BEFORE it is made
  // by the loading application
  cy.server()
  cy.route('/todos', 'fixture:empty-title')
  cy.visit('/')
})

The test runs - and the application is showing the empty title, albeit imperfectly

Application showing Todo with empty title from the stubbed network response

Now you can style the application if necessary or take any other precautions against empty titles. The important point is that the built-in Cypress Network stubbing allows you to test how your application is handling the cases of data that are impossible to create via the current UI.

Full control with App Actions

To test a data edge case we can reach directly into the application at run-time and create a data item using what we call App Actions. From the application's code store the reference to the app on the window object when the application is running inside a Cypress-controlled browser like this

// somewhere in your application code
const app = ...
if (window.Cypress) {
  window.app = app
}

Because Cypress test code runs in the same browser window, you can access the application's window object, then grab the app property - and now the tests can manipulate the app reference directly. For example, our web application is implemented using Vuex, thus there is app.$store object with actions the application code dispatches. But our test code can use the same object to dispatch its actions, bypassing any UI restrictions.

it('handles todos with blank title', () => {
  // stub the initial data load
  // so the application always starts with 0 items
  cy.server()
  cy.route('/todos', []).as('initial')
  cy.visit('/')
  // make sure the network call has finished
  cy.wait('@initial')

  // bypass the UI and call app's actions directly from the test
  // app.$store.dispatch('setNewTodo', <desired text>)
  // app.$store.dispatch('addTodo')
  cy.window()
    .its('app.$store')
    .invoke('dispatch', 'setNewTodo', ' ')

  cy.window()
    .its('app.$store')
    .invoke('dispatch', 'addTodo')

  // confirm the application is not breaking
  cy.get('li.todo').should('have.length', 1)
    .find('label').should('have.text', ' ')
})

In the above test, we start with zero items after the initial Ajax call completes - by stubbing it and waiting for it. Then we dispatch two actions directly to the application's Vuex data store, creating a Todo item with a single space character as a title. Since the App Action is invoked directly against the data store, the UI check is bypassed. Finally, the test confirms the single Todo with the blank label is displayed.

The passing test

In this blog post I showed how to test edge cases in the data that your application might have to handle; the edge cases that are impossible to recreate by going through the user interface anymore, because the application has tightened the input data validation rules. We have confirmed that blank titles can no longer be entered, but if a Todo item with a blank text does get shown it does not break the application (aside from looking unprofessional).