Test State Management in Cypress (Part 7)

When I first started building this Cypress framework, I was barely exploring what things cypress has to offer out of the box and few missing things which can be obtained with the help of external NPM packages. Once I was confident enough with the framework I started experiencing other challenges while trying to reuse my generated data; so the need for State Management arose.

Test State Management is essentially how to manage the data which is generated by the the framework which is to be used in subsequent test steps

The Challenge

So I had a framework which was running and consuming and generating test data.
In order to consume data which was generated from a step in subsequent step, I was lost for how to do so with Cypress.
Cypress does provide something called Variables & Aliases or Environment Variables out of the box, but using which to derive the expected state/resource could be a cumbersome process.

For instance, you created a user and wanted to use its generated user_id in subsequent API test step:

//Step #1 : Create a user
cy.request({
    method: 'POST',
    url: '/user',
    headers: {
      'Content-Type': 'application/json'  
    },
    body: {
    "userName":Cypress.env('users')[userType.toUpperCase()].email,
    "password":Cypress.env('users')[userType.toUpperCase()].password,
    "Name": Cypress.env('users')[userType.toUpperCase()].password}
  }).as('createUser')

//Step #2 : retrieve user ID to update profile image
  cy.get('@createUser').then((response)=> {
    expect(response.status).to.eq(201);
    let user_id = response.body.user_id;
	//using user_id here to update profile image
    
  })
Sample API code for creating a user and retrieving it using Cypress Aliases

This may seem simpler from the example, but as the steps increase and you need to keep track of all the aliases which were created to get data and reference them each and every time. We will look how this can be tackled by creating a simple JSON store.


The Solution:

In order to start you can clone the repo here, as I will be using this as a base for building our Test State Management.
We'll start by adding an empty store.json file under cypress/fixtures/state folder.

creating store.json file

Now in order to get and store values inside this json file we will add 2 custom commands inside support/command.js file

First install lodash package using command line:

npm install lodash --save-dev
npm package install command for Lodash

The saveState() requires a key-value pair to save inside the store.json

//import Lodash to commands.js
const _ = require('lodash');


/**
 * This Command stores a value to Test State
 * 
 * @param {String} Key - Key to be stored
 * @param {String} Value - Value to be stored
 */
 Cypress.Commands.add("saveState", (key, value) => {
  cy.log(key, value);
  if(key.includes(">")){
    let keyItems = key.split(">");
    cy.readFile('cypress/fixtures/state/store.json').then((currState) => {
      let newState = currState;
      _.set(newState, keyItems, value);
      cy.writeFile('cypress/fixtures/state/store.json', newState);
    })
  }else{
  cy.readFile('cypress/fixtures/state/store.json').then((currState) => {
    currState[key] = value;
    cy.writeFile('cypress/fixtures/state/store.json', currState);
 })
}

});
cy.saveState() command implementation

Usage:

 cy.saveState("page_id", response.body.page.id); //stores as JSON page_id key value
 cy.saveState("user>user_id", response.body.payload.id); //stores user_id inside user JSON object

Result:

{
	"page_id": "asd67asdfsa",
	"user":{
    	"user_id": "Ka6ds"
      }
}
store.json after executing above commands
Here ">" is the special character which is used for signifying the hierarchy

Now similarly we'll add another command getState() to retrieve a stored value by using a stored key

/**
 * This Command retrieves a param value stored in Test State
 * 
 * @param {String} Key - stored param key
 */

 Cypress.Commands.add("getState", (key) => {
  if(key.includes(">")){
    let keyItems = key.split(">");
    cy.readFile('cypress/fixtures/state/store.json').then((state) => {
      return _.get(state, keyItems);
    })
  }else{
    cy.readFile('cypress/fixtures/state/store.json').then((state) => {
      return state[key];
    })
  }
});
cy.getState() command implementation

Usage:

//retrieve page_id from the store
cy.getState("page_id").then(value => {
	let pageId = value;
    console.log(pageID);  // it will print asd67asdfsa
    // now we can use pageId where ever its needed
}); 

//retrieve user_id from user json object
 cy.saveState("user>user_id").then(value => {
    let userId = value;
    console.log(userID);  // it will print Ka6ds
 });

Once above changes are made into the framework we will start by adding a new feature file.

New Feature file for GET /pokemon/{id}
 @API
 Feature: Pokemon GET /pokemon/{id} by id
    @smoke @test
    Scenario: Fetch data for a pokemon using API and verify it
        Given As a user I want to execute Pokemon GET api for Pokemon "pikachu"
        Then Verify '@get_pokemon_data' response status code is 200
        When I save the user id in Test Store
        And I make a GET request on '/pokemon/{id}' endpoint with the stored id
        Then Verify '@get_pokemon_data_by_id' response status code is 200
        And Verify response details for Pokemon "pikachu"


New feature file content
in apiSteps.js add following code for stepDefinition for 2 new steps
  When('I save the user id in Test Store', () => {
    cy.get('@get_pokemon_data').then((response)=> {
      //save pokemon id
      cy.saveState("PokemonData>PokemonID", response.body.id)
    })
  
  });
stepDefinition for step "When I save the user id in Test Store"

  When('I make a GET request on {string} endpoint with the stored id', () => {
    cy.getState("PokemonData>PokemonID").then(pokeID => {
      cy.request({
        method: 'GET',
        url: 'https://pokeapi.co/api/v2/pokemon/' + pokeID,
        headers: {
          'Content-Type': 'application/json'  
        },
        failOnStatusCode:false
      }).as('get_pokemon_data_by_id')
    })
  });
stepDefinition for step "When I make a GET request on {string} endpoint with the stored id"

That's it. When you run this feature file from the cypress runner that it first fetches the data from GET /pikachu/{name} endpoint and stores the pokemon_id from the response into our store.json and then in subsequent step uses this stored value to fetch data from GET /pikachu/{id} endpoint

store.json after running the feature file

Final Notes:

We could additionally add a clearState() command which could be run at the beginning or end of a test run.

/**
 * This Command clears the Test State
 * 
 */
 Cypress.Commands.add("clearState", () => {
  cy.readFile('cypress/fixtures/state/store.json').then((currState) => {
    currState = {};
    cy.writeFile('cypress/fixtures/state/store.json', currState);
 })
  cy.log("Test state was reset");
});
clearState() command implementation

Caution: using cy.clearState() will remove everything from store.json

Full code on:
https://github.com/far11ven/Cypress-TestFramework/tree/develop/Part 07