Integrated Mocking Management and cypress’s Mocking Toggle Feature

TOAST UI
7 min readJun 8, 2021

Mock data refers to artificial data created by developers for testing purposes. When are mock data most commonly used?

  • When it is hard to pin down expected values in E2E testing environment due to the dynamic nature of the actual data
  • When it is hard to assess different data representations in the UI testing environment

Test codes require mock data, and at some point, you can easily find similar mock data scattered around the project.

This article serves to record my experience managing mock data in a single integrated environment as well as cypress’s mocking toggle feature.

General Mocking With Storybook and cypress

For the project that I am participating, we are using storybook and cypress for developed UI testing and E2E testing, respectively. First, let’s discuss how each tool can be used to create mock data.

Mocking With Storybook

Because the Storybook does not offer a built in function for API mocking, a third party service is necessary. For our project, we are using axios-mock-adapter which is a part of axios.

We could create mock API for each story to check each UI, but I went ahead with all mocking at once in order to view data-complete stories, only.

Mocking from .storybook/preview.js file

Mocking With cypress

cypress provides a separate intercept method for mocking. This method can be used to simulate data that fits each test case appropriately. Therefore, we mock the API that is needed inside of the test function and assess the responses with respect to the mock data instead of actual data.

Necessary mock data are created in the fixtures folder as a JSON file.

Mocking inside each test function

What’s The Problem?

Both Storybook and cypress needed API mocking, and this created redundant code and data.

Storybook’s axios-mock-adapter and cypress’s intercept have different interfaces. This means that if there is were to be any changes to the API or to the response, we would need to make necessary changes to mockings for both Storybook and cypress.

Then is there a way for us to manage modules that mock API responses in a singular location? This made me want to build an 'Integrated API Mocking Management Module'.

Building the Integrated API Mocking Management Module

First, let’s lay out some requirements for each tool.

  • For Storybook, it needs to be able to produce mock API that corresponds to the stories in order to test that the UI fits the story. Also, it needs to be able to mock all APIs.
  • For cypress, it needs to be able to produce mock API as is expected by the test function, as well as ensure that the expected value is displayed to the user afterwards.

By looking at our requirements, it is clear that we must be able to control both the content and the quantity of mock data from the outside. In other words, the data must be created dynamically.

Integrating Mock Data

We use a function to create mock data dynamically from static mock data and the desired quantity. The return type of the function will be according to the project’s API format.

Here, our project’s API response is as follows.

{
result: {
contents: [
{
// ...
}
]
}
}

Let’s take a look at the createFooMock function example that mocks the /api/foo API.

We create a createContentsMock in order to create list format mock data (contents:[]). The first parameter for createContentsMock is a createMock function that returns a single mock item, and the second parameter is count, the number of mock data to be produced.

Next, we create a function that takes in some values and creates mock data in order to control mock data from the outside for certain APIs.

Now, we can call the createFooMock function to create mock data for mocking /api/foo API from anywhere.

Integrating Mocking Modules

Let’s create a new module that integrates different mocking module interfaces which will allow us to work in a singular location when any changes to the interfaces occur.

The code is a little complicated. Let’s take break it down bit by bit.

First, we write the mockSystem function that creates the integrated mocking management object. Its parameter, mockAdapter, is used to receive different mocking tools. For Storybook, the axios-mock-adapter will be passed as the argument, and for cypress, the cy.intercept will be passed as the argument.

export function mockSystem(mockAdapter) {
// Mocking tool that includes the integrated interface
return {
onGet(){},
onPut(){},
// ...
}
}

If the tool received from the mockAdapter has an on method, it will follow the axios-mock-adapter interface, and if not, it will follow the cy.intercept interface.

function hasOnMethodProperty(adapter) {
if ('onGet' in adapter) {
return true;
}
return false;
}
export function mockSystem(mockAdapter) {
// Mocking tool that includes the integrated interface
return {
onGet() {
if (hasOnMethodProperty(mockAdapter)) {
// axios-mock-adapter method
}
// cy.intercept method
}
// ...
}
}

The onGet method in the integrated mocking management object takes the API's path information as its first parameter. Its second parameter, mock, is the mock data. The mock data, static and dynamic, will be converted into functions with the createMockFunction function.

Next, each conditional will fill in the codes according to the axios-mock-adapter and cy.intercept methods.

function createMockFunction(mock) {
return isFunction(mock) ? mock : () => mock;
}
export function mockSystem(mockAdapter) {
// Mocking tool that includes the integrated interface
return {
onGet({ path }, mock = {}) {
const mockFn = createMockFunction(mock);
if (hasOnMethodProperty(mockAdapter)) {
// axios-mock-adapter method
}
// cy.intercept method
}
// ...
}
}

If you take a detailed look at the entire code, when mocking onGet method, we pass a function as the reply method's parameter.

export function mockSystem(mockAdapter) {
return {
onGet({ path }, mock = {}) {
// ...
if (hasOnMethodProperty(mockAdapter)) {
return mockAdapter.onGet(path).reply(({ params }) => {
return [200, mockFn({ params })];
}); // axios-mock-adapter method
}
// ...
},
onPost({ path }, mock = {}) {
// ...
if (hasOnMethodProperty(mockAdapter)) {
return mockAdapter.onPut(path).reply(200, mockFn());
}
// ...
},
};
}

The reason we pass a function as the reply method's parameter is that when dealing with an actual API, it may return different data according to the query string value, so the mock data must also be able to vary. We can mock it as shown below.

Now, using the mockSystem function, we can add or edit the mock data at mockSystem module even if we were to add different mocking modules.

Mock APIs

Let’s proceed to mock APIs with the integrated mock data and mocking module.

First, ensure that the API names do not overlap by prefacing it with a namespace and making it a constant. This way, when all of the mock APIs have been integrated, we can prevent duplicate name errors.

export const FOO = 'NAMESPACE/FOO';
export const FOO_BAR = 'NAMESPACE/FOO_BAR';

Then, we write a createFooMockApi function that accepts the integrated mocking module, mock. This function returns an object that can mock APIs related to the Foo. Here, we collect the related APIs together while separating the APIs by methods in a separate object. This is to make our job easier when we get to Using It With cypress.

Integrating Mock API

Now that we’ve created a createFooMockApi for mocking special APIs, let's create the createMockApi function that returns addAllMockApi function for mocking all APIs at once from Storybook and mockApi object that calls APIs one at a time from cypress.

Here, since all mock APIs are integrated into one, we need to make sure that there are no overlaps in API namespace.

Let’s see how we can use the functions we wrote so far.

Using the Integrated API Mocking Management Module

Using It With Storybook

All we have to do in Storybook is call the addAllMockApi function, then we will be able to go about it as if everything else were the same.

When we pass the MockAdapter, the mocking module used with Storybook, as the mockSystem function's argument, it will return an object with integrated interface to be used for API mocking.

Using It With cypress

With cypress, we pass the cy.intercept, the function used for mocking, to mockSystem function's argument. Then, pass the constructed integrated mocking module to createMockApi function to get the mockApi object that can mock all APIs.

The mockApi is used in the text body as shown below.

https://gist.github.com/414559020e39f4a9efb7181ac98f669c.git

But separating each module is more convenient than calling the createMockApi function for each test.

The provided modules can be used for actual testing as follows.

cypress’s Mocking Toggle Feature

The ultimate goal of E2E testing that I have in mind integrates backend as well by building a test server to simulate real-user-experience. Then, the test stops being a simple frontend E2E test but becomes a E2E test that spans over the whole system, increasing credibility of the entire test code. Therefore, before we actually build a test server, we use mock APIs for testing. When the test server is online, we can collect the mockings from all test functions and toggle them off to proceed directly with the E2E test with a test server.

Solution

cypress’s config can be used to control such actions in a centralized location.

Writing cypress.json

{
//...
"env": {
//...
"mockApi": true
}
}

Even if we turn off mocking, the cy.mocks.get(FOO, {}) interface used in test functions must be valid, so we must configure it so that the only the API responses are affected. Additionally, cypress intercept method does not allow overwriting function calls from the test function (solution is demonstrated in Cypress cy.intercept Problems), so we must write a separate http custom command that wraps the intercept method.

Conclusion

We create mock data for faster development and safer maintenance. It is horrifying when we have to meddle with mock data when we have so much development to do.

The Integrated API Mocking Management method has the advantage of having a centralized location to manage the mock data even if the API or the API response were to change. Also, it will allow developers to react more flexibly when new mocking modules emerge. Quantitatively, I was able to reduce 88 lines of mocking code in half, and every time there was a change to API responses, I could edit the code faster, thereby increasing convenience.

The mocking toggle feature suggests a way for us to expand our range of testing from just frontend to frontend and backend.

I hope that this article can help the developers using mock data.

--

--