Mocking server resources is very often a necessity during development. This might be due to various reasons: the server is down or unavailable, or your API is still under development and not fully functional. You might not be able to afford waiting for the problem to be resolved or you might want to plan Frontend development in parallel with Backend development using a predefined contract.
在开发过程中,通常必须模拟服务器资源。 这可能是由于多种原因造成的:服务器已关闭或不可用,或者您的API仍在开发中并且无法完全正常运行。 您可能无法承受等待解决问题的费用,或者您可能希望使用预定义的合同在进行前端开发的同时计划前端开发。
Those are valid reasons and they might make you look for solutions. So, how would you go about solving this problem?
这些都是正当的理由,它们可能会让您寻找解决方案。 那么,您将如何解决这个问题?
一些现成的选择 (Some off-the-shelf options)
First, you would need to either find a ready-made mock library or write your own. Let’s see what options are available.
首先,您需要找到现成的模拟库或编写自己的模拟库。 让我们看看有哪些可用的选项。
Requests can be mocked on the server-side, by a plugin in the browser or in the application itself. Let’s see that more in detail.
可以在服务器端,浏览器中的插件或应用程序本身中模拟请求。 让我们更详细地看看。
One possible option would be to use a browser plugin, such as Requestly¹. Here is an article describing this approach:
一种可能的选择是使用浏览器插件,例如Requestly¹ 。 这是描述此方法的文章:
Requestly allows the interception of server requests in the browser and redirects them to an alternative location, such as a local server with static JSON files. This setup would at minimum require setting up a static server (using node.js Express or any other server serving static files). While this is fine for your local development, working in a team would require that each developer create a local setup on each machine, which is not so practical. Ideally, you would like to be able to share the configuration between the users.
Requestly允许在浏览器中拦截服务器请求,并将它们重定向到其他位置,例如具有静态JSON文件的本地服务器。 此设置至少需要设置静态服务器(使用node.js Express或提供静态文件的任何其他服务器)。 尽管这对于您的本地开发很好,但是在团队中工作将需要每个开发人员在每台计算机上创建一个本地设置,这不太实际。 理想情况下,您希望能够在用户之间共享配置。
If you are looking for libraries which enable server resource mocking on a node.js server, a library called nock² comes to mind. It allows you to mock server requests made by a script running on a node server. This might be a fine solution for unit testing purposes, when testing components which are dependent on external server requests. Mocking removes the need to have any real network connectivity and real access to the resources. This increases the reliability, speed and relevance of the unit tests. A way of testing React components using nock is described here: https://itnext.io/nock-it-out-of-the-park-http-mocking-for-react-42ec927f83e0
如果您正在寻找在node.js服务器上启用服务器资源模拟的库, 那么就会想到一个名为nock²的库。 它允许您模拟由节点服务器上运行的脚本发出的服务器请求。 当测试依赖于外部服务器请求的组件时,这对于单元测试而言可能是一个很好的解决方案。 模拟消除了对任何真实网络连接和对资源的真实访问的需求。 这提高了单元测试的可靠性,速度和相关性。 此处介绍了使用nock测试React组件的方法: https : //itnext.io/nock-it-out-of-the-park-http-mocking-for-react-42ec927f83e0
Another possibility is Service Mocker³, a mocking solution using service workers. This is worth looking into, but it is not ready for prime-time as of this writing.
另一种可能性是ServiceMocker³ ,这是使用服务人员的模拟解决方案。 这是值得研究的,但是在撰写本文时,还没有准备好黄金时段。
致力于定制解决方案 (Working towards a custom solution)
All this is fine and good, but what if you just want to run your application on localhost as a user, simulating an end-to-end experience, even though your backend is not ready? The endpoints that you need either don’t exist completely, or return an older version of the API, one that you cannot use. You might need some new kind of data while developing, in order to test how your application reacts to it. At the same time, it would be nice if the setup was part of the codebase itself and was as unobtrusive as possible, while having a low barrier of entry. Only the required parts would be mocked („mock as you go”), while all the rest would work as normal. A request to a missing mocked resource should just redirect to the original, while the consumer code would be unchanged. A representation of the request payload also needs to be taken into account, in order to allow different responses to different request parameters.
所有这些都很好,但是,如果您只是想以用户身份在localhost上运行应用程序,模拟端到端的体验,即使您的后端尚未准备好怎么办? 您需要的端点不完全存在,或者返回的API较旧版本无法使用。 在开发过程中,您可能需要一些新的数据,以测试您的应用程序对此有何React。 同时,如果安装程序是代码库本身的一部分并且尽可能不引人注目,同时具有较低的进入门槛,那将是很好的选择。 仅对所需的部分进行了模拟(“随手可做”),而其余所有部分都将正常运行。 对丢失的模拟资源的请求应仅重定向到原始资源,而使用者代码将保持不变。 为了允许对不同请求参数的不同响应,还需要考虑请求有效负载的表示。
使用可观察物 (Using observables)
Let’s see how the above requirements could be solved in the context of a real-world application using React, Redux and Rx.js observables, all bundled with Webpack.
让我们看看如何在实际应用程序的上下文中使用React,Redux和Rx.js Observables(都与Webpack捆绑在一起)解决上述要求。
Note: in this article we are not going to discuss the way to set up Redux with observable middlewares. For an in-depth discussion about this topic, please see:
注意 :在本文中,我们将不讨论使用可观察的中间件设置Redux的方法。 有关此主题的深入讨论,请参阅:
Our mocking solution required injecting requests for mocked resources into original observable streams. When in mock mode, the application would first attempt to resolve a mocked data stream and serve it to the consuming code, or — if it fails — it would fall back on the original data stream. This can be accomplished with the catchError rxjs operator. This is a visual representation of the stream generated by this operator:
我们的模拟解决方案要求将对模拟资源的请求注入到原始可观察流中。 当处于模拟模式时,应用程序将首先尝试解析模拟的数据流并将其提供给使用的代码,或者-如果失败,它将退回到原始数据流。 这可以通过catchError rxjs运算符完成。 这是此运算符生成的流的直观表示:
Source: https://rxjs-dev.firebaseapp.com/api/operators/catchError
来源: https : //rxjs-dev.firebaseapp.com/api/operators/catchError
So, in order to achieve our goal, we need to combine two observable streams: the mocked data stream and the original data stream. If the first fails, the second takes over. Below is a simple implementation of this concept in a function called ajaxWithMock which wraps a simple ajax observable with a mocked ajax observable.
因此,为了实现我们的目标,我们需要结合两个可观察的流:模拟数据流和原始数据流。 如果第一个失败,则第二个接管。 以下是在名为ajaxWithMock的函数中该概念的简单实现,该函数将可观察的简单ajax与可模拟的 ajax可观察的包装在一起。
import { ajax } from 'rxjs/observable/dom/ajax';
import { catchError } from 'rxjs/operators';
const ajaxWithMock = ({ method, url }) => {
const mockUrl = 'http://localhost/resource';
const realObservable = ajax({ method, url });
const mockObservable = ajax({ method: 'GET', url: mockUrl });
return mockObservable.pipe(
catchError((err) => realObservable);
);
}
Next, we need to use the redux-observable⁴ library to plug our middlewares:
接下来,我们需要使用redux-observable⁴库来插入我们的中间件:
import { createEpicMiddleware, combineEpics } from 'redux-observable';
import { ajaxWithMock } from './ajaxWithMock';
const combinedEpics = combineEpics(
// insert someEpic,
// and someOtherEpic,
);
export const ajax = {
get: (url) => ajaxWithMock({ method: 'GET', url }),
post: (url) => ajaxWithMock({ method: 'POST', url }),
};
export const createRootEpic = catchError => createEpicMiddleware(combinedEpics, {
dependencies: {
ajax,
},
});
What remains to be done, is to attach our middleware to the redux flow:
剩下要做的是将我们的中间件附加到redux流:
import { createStore, applyMiddleware } from 'redux';
import { createRootEpic } from './createRootEpic';
export const createRootStore = (initialState = {}) => {
const middlewares = applyMiddleware(
createRootEpic(),
);
return createStore(
// insert rootReducer here,
initialState,
middlewares,
);
};
The consumer code in the epic itself can now be used without any knowledge of the underlying mock system. Just use ajax calls to the original resource as usual! If a mocked resource is present, the response will contain mocked data. Normal error handling for the original resource will still work.
史诗本身中的使用者代码现在可以在不了解底层模拟系统的情况下使用。 像往常一样使用对原始资源的ajax调用! 如果存在模拟资源,则响应将包含模拟数据。 原始资源的正常错误处理仍将起作用。
import { ofType } from 'redux-observable';
import { map, switchMap } from 'rxjs/operators';
import * as endpoints from './endpoints';
import * as actions from './actions';
export const someEpic = (action$, store, deps) =>
action$.pipe(
ofType(actions.SOME_ACTION),
switchMap(() =>
deps.ajax.get(endpoints.SOME_ENDPOINT)
.pipe(
map(({ response }) => {
// do something with the response
}),
),
),
);
The code that we have now attempts to send requests to our mocked server URL before trying to send a request to the actual endpoint. In the example below, you will see original URLs with folder structures, as well as mock URLs with flattened name structures (slashes have been replaced with underscores). The mocked files contain a suffix which will be explained further in this article. Here is how this looks in the network console:
现在,我们尝试在将请求发送到实际端点之前将代码发送到模拟服务器URL的代码。 在下面的示例中,您将看到具有文件夹结构的原始URL以及具有扁平名称结构的模拟URL(斜杠已被下划线代替)。 模拟文件包含后缀,本文将对此进行进一步说明。 这是在网络控制台中的外观:
As you can see, some requests were satisfied directly from the localhost because a mock file was present where expected, while others attempted to retrieve a resource from the localhost, failed with a 404 and proceeded to connect to the original resource, just like we planned. This has an important benefit for the users of the mock system: they do not have to mock all of the endpoints in order to start using mocked resources. All they need to do is to mock the resources they need at a given moment. This greatly helps with easy adoption in the team.
如您所见,某些请求直接从本地主机得到了满足,因为在预期的位置存在一个模拟文件,而其他请求则尝试从本地主机检索资源,以404失败并继续连接到原始资源,就像我们计划的那样。 这对模拟系统的用户来说具有重要的好处:他们不必为了开始使用模拟资源而对所有端点进行模拟。 他们需要做的只是在给定时刻模拟所需的资源。 这极大地有助于团队轻松采用。
处理请求参数 (Handling request parameters)
So, now we have our mock infrastructure in place. However, what is missing is any handling of the request parameters. So let’s take care of that.
因此,现在我们有了模拟基础结构。 但是,缺少对请求参数的任何处理。 因此,让我们来照顾它。
This requires some thought. If a static file server is to be used, it is better to avoid the processing of POST requests with all of their parameters. At the same time, we want to be able to mock POST and GET requests in a transparent manner, taking into account the parameters passed in the payload and in the URL.
这需要一些思考。 如果要使用静态文件服务器,最好避免使用所有参数处理POST请求。 同时,我们希望能够以透明的方式模拟POST和GET请求,并考虑到有效负载和URL中传递的参数。
This can be accomplished by using a hash digest of the payload, in order to create a string representation of whatever was passed to it.
这可以通过使用有效负载的哈希摘要来完成,以便创建传递给它的内容的字符串表示形式。
const hash = require('custom-hash');
module.exports.hashDigest = (payload) => {
hash.configure({ charSet: ['A', 'B', 'C', 'D', 'E', 'F'], maxLength: 6 });
return hash.digest(payload);
};
Note: The common.js require format was used here, so that our hashing function can be used both in node.js scripts and in the frontend application built on top of Webpack.
注意:此处使用了common.js require格式,因此我们的哈希函数既可以在node.js脚本中使用,也可以在基于Webpack的前端应用程序中使用。
Assuming that the static files are served from http://localhost:3001, the mocked URL can be constructed this way:
假设静态文件是从http:// localhost:3001提供的 ,则可以通过以下方式构造模拟的URL:
const payload = hashDigest(JSON.stringify(query));
const fileName = `${Url}`
.replace(/(?!^)(\/)/g, '_')
.replace('/', '');
const mockUrl = `http://localhost:3001/${fileName}_${payload}.json`;
So, a request with a specific payload will be transformed in such a way:
因此,具有特定有效负载的请求将以以下方式转换:
A GET request of this type: https://example.com/company/getAllCompanyCategories?language=english will be transformed into: http://localhost:3001/company_getAllCompanyCategories_CBCDDD.json where CBCDDD is a hashed representation of the query string parameters.
此类型的GET请求: https : //example.com/company/getAllCompanyCategories?language=english将转换为: http:// localhost:3001 / company_getAllCompanyCategories_CBCDDD.json ,其中CBCDDD是查询字符串参数的哈希表示。
What remains to be done, is to create static JSON resources and serve them from the requested locations. This can be done manually just by placing appropriately named files in the required locations or via scripting. However, this is not covered by this article.
剩下要做的是创建静态JSON资源,并从请求的位置提供这些资源。 只需将适当命名的文件放在所需位置或通过脚本,即可手动完成此操作。 但是,本文未涵盖此内容。
翻译自: https://medium.com/ecovadis-engineering/mocking-server-resources-in-frontend-development-59082a2b6426