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
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.