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.
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.
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(...)
}
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.
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:
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)
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>
);
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:
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>)
}
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.
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:
{
"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"
}
]
}
}
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:
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 😉
This original blog post was written by me for Rookout here https://www.rookout.com/blog/how-to-use-mock-data-without-changing-internal-logic