Integrated Mocking Management and cypress’s Mocking Toggle Feature

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

Mocking With Storybook

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

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

Mocking inside each test function

What’s The Problem?

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

  • 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

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

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

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

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

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

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

Solution

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

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.

JavaScript UI Library Open Source by http://ui.toast.com