DEV Community

Cover image for How To Use Mock Data Without Changing Internal Logic
Ryan Hoffman
Ryan Hoffman

Posted on • Edited on • Originally published at rookout.com

How To Use Mock Data Without Changing Internal Logic

In every job and every company, that day always comes where you find that you…actually need to do some work. And by that, I mean finding new innovative solutions to tasks and projects instead of going with the tried-and-true methods. Recently, my team, of full stack developers were tasked with building a webview app to support our live debugging IDE plugin. This would allow us to align the user experience with our pre-existing web app. Using the IDE plugin’s UI framework meant we would need to rewrite a lot of the code, which would also result in a very different user experience due to the differences between frameworks and a lot of engineering time which means a lot of money.

Obviously, that’s not the ideal choice. So we decided to take a bit of a different approach. You know, one that would make our lives easier and help us create a better product, faster, and maintain the high-level of user experience our customers expect.

A New Approach

In order to be aligned with our web application we decided to shift from using our plugin’s UI system (Swing in Jetbrains case) and use a modern frontend framework (React) which is much better at rendering – and more importantly, re-rendering – a lot of UI elements. The decision also brought about many smiles in our team as it was easier for us to develop due to us having more experience developing with Javascript than Java Swing. Our goal and mindset was to develop quickly, efficiently, and with as little friction as possible and not be coupled to the plugin by having to rerun the plugin with every change to the UI code.

With that goal in mind, well..we all know the saying of ‘people plan…’, right? As with every good new feature or product, we encountered problems which were unrelated to the webview but impacted development efficiency.

Speeding Up Code Development

In order to overcome these issues we chose to take an unconventional route of building mock data responses. These are predefined data sets that mimic the data we would have received from other services, while overriding native communication functions such as “fetch”. This solution dealt with many of the issues we wanted to tackle, such as keeping our code clean and allowing each part of the whole system to be developed separately.

Keeping services decoupled from one another is a best practice in backend development when working with microservices.It allows complete autonomy of development and deployment per service, which means that as long as we are backwards compatible it is possible to continue to advance and deploy our UI without taking into account the missing correspondent changes from the plugin (other services). When using mock data, it’s common to succumb to the pull of changing your code so as to be aware that mock responses are being used. However, this dirties up the code and can cause future bugs.

For example, the code block below demonstrates specifically how the code is aware of the usage of mock responses. We indicated that if we are in a development environment it should return mock data.

if (env === "development"){
return  {...mockData} <-- local response
} else {
 return fetch(...)
}
Enter fullscreen mode Exit fullscreen mode

In contrast to what we see in the code block above, what we would like to achieve is the ability to return mock data and not make our internal logic aware of the fact that it is getting mock data.

So What Do We Do?

Using mock responses has been a great tool for me as it has allowed me to continue developing my features or fixing bugs without being dependent on the development and prioritization of others. For example, what might be a major issue on my end could be a minor issue on their end and vice versa. Specifically at Rookout, we develop the IDE plugin at the same time as the Web UI, which acts as an individual service that is dependent on data received from the plugin. With the mock responses I don’t have to wait for development on the plugin to finish before I can start working on my own tasks.

The top two options that I personally employ when I need to use mock responses are:

  1. Creating a local web server that will take all requests and return the data as if it was the missing agent. While this solution works great, it requires some setup and for the developer to have a certain level of knowledge (basic as it may be) in backend development.

If the logic is kept untouched it will always point to whatever env.backendUrl stores, which by environment will point to your local machine or production machine.

// dev: localhost://...
// prod: https://...
const url = env.backendUrl
return fetch(url
Enter fullscreen mode Exit fullscreen mode
  1. The second option – and my main focus for this article – is overriding the original behavior of the api we are looking to use (i.e fetch).

In modern frameworks we have a main.js/ts file which handles the initial load of our web application. This is the area where we can do our magic.

window.fetch = function (request) {
   return new Promise(resolve => {
     if(typeof request === "string") {
       return resolve({
         isGetMethod: true
       })
     } else {
       const response  = {
         isGetMethod: request.method === "GET",
       }
       if(request.method === "POST") {
           response["body"] = async () => {
             return {
               ...mockData
             }
           }
       }
       return resolve(response)
     }

   })
 }

const root = ReactDOM.createRoot(
 document.getElementById('root') as HTMLElement
);

root.render(
 <StrictMode>
   <App />
 </StrictMode>
);

Enter fullscreen mode Exit fullscreen mode

In the code block before the app loads, we override the fetch api with our custom behavior. Any fetch done in our react app will get to our new implementation instead of the original fetch api.

An explanation of what the demo above does:

  1. If the request’s method is “GET”, return {isGetMethod: true}
  2. If the method is “POST” , return as an async function (implementation of res.body()) which returns the mock data we want to receive.

In order to show the result let’s take a look at the simple component:

export const Comp = () => {

useEffect(() => {
 fetch({
   url: "https://jsonplaceholder.typicode.com/todos/1",
   method: 'GET'
 }).then(console.log) <-- example of response with get

 fetch({
   url: "https://jsonplaceholder.typicode.com/todos",
   method: 'POST',
   body: {
     "title": "demo"
   }
 }).then(console.log) <-- example of response with post

 fetch({
   url: "https://jsonplaceholder.typicode.com/todos",
   method: 'POST',
   body: {
     "title": "demo"
   }
 }).then(async (res) => {
   console.log(await res.body()) <-- example of parsing body response
 })

}, [])


return (<div>A Comp</div>)
}

Enter fullscreen mode Exit fullscreen mode

The component sends requests to “production” endpoints, but the response is dealt with locally. Amazingly, no internal code change was made and reduced probability to produce new bugs while deploying to production.

WAIT A MOMENT!

A change in main.js/ts was made! How do we deal with it?

Popular Frontend frameworks as React/Angular (and NX as a mono-repo manager) supports using different “main” files when using different configurations for building and serving our application, and thus we can make two “main” files:

  1. main.js/ts
  2. main-mock.js/ts
"mock": {
         "extractLicenses": false,
         "optimization": false,
         "sourceMap": true,
         "vendorChunk": true,
         "main": "apps/demo/src/main-mock.tsx",
         "fileReplacements": [
           {
             "replace": "apps/demo/src/environments/environment.ts",
             "with": "apps/demo/src/environments/environment.mock.ts"
           },

         ]
       },

Enter fullscreen mode Exit fullscreen mode

The default is our main.js/ts (kept untouched) and it is our main-mock for when we build with the mock configuration.

This will provide two big benefits:

  1. The original main file is kept untouched.
  2. Code added for mock responses will not be bundled when building production configurations and as a result will not affect customer facing bundle size (will not impact performance).

Why is all this work worth it?

Simply put? It’s a game-changer for quick and efficient development.

When we want as a team to develop multiple features simultaneously, we need the ability to work as independently as possible from one another but to also be aware of the fact that in production all parts will be – and are – connected. Each of us affects the other, whether we see it or not.

Using mock data responses by overriding our communication functions (such as fetch) allows us to keep our code unaware of the fact that its being fed mock data and will allow us to develop new features quickly and safely while also giving us the opportunity to debug issues that might occur.

A happy bonus to this pattern is that usually, as a result of the separation of concerns in development, the code we write is much less error prone and can handle almost any data thrown at it. Therefore, it is also much easier to replicate edge cases and create unique handlers for such cases. And if a bug does somehow occur, well that’s what Rookout is for: production debugging, so that you can see what caused the bug and replicate it with mock responses.

The idea of parallel work is something we all wish to achieve as a team, but very hard to implement. By using mock data, we are now one step closer to achieving it.

Give it a try. Happy coding 😉

Top comments (0)