A least dependent, localized, with version control and effect immediately http / websocket api mock tool for front-end project, nodejs back-end project, or even general back-end project if you want.
- mock-api
- Why this mock-api?
- How is it working?
- Installation
- Usage
- Tips or Troubleshooting
- Await features...
- least dependent
- least coordination with other developers in coding all your project
- the only information you need is the api defination: method, path, queries, reponse structure etc.
- and of course you may mock any response as you want, such as an error response specifically
- localized
- no bother to start and maintain a mock service on a virtual machine
- start mocking just as you start your project development, e.g.
npm run serve
if your project is a vue-cli-serivce project
- with version control
- all of the mock configuration stay with your project source code version control, so that after half year, for example, you still can use the mock configuration configured now.
- effect immediately
- almost all config can take effect immediately, e.g.
- a new mocking api path
- a response file amending
- a
ws-response.js
file change - and even the proxy404 file
- almost all config can take effect immediately, e.g.
- able to mock websocket
The design is simple. 'mock-api' starts a service on local computer, respond to request
according to mock configuration. Mock configuration should be placed on fake-services
folder
on project root directory.
'Mock-api' will create a file named .mockingLocation
on project root to indicate the http
mocking location(replace string 'http' with 'ws' to get websocket mocking location).
'Mock-api' exports a function which returns a promise that will resolve the
mocking server. Mocking srever is an instance of
http.Server.
mockingServer.getHttpLocation()
to get http mocking location,
mockingServer.getWsLocation()
to get websocket mocking location.
How to write a mock configuration is described below.
How to use .mockingLocation file properly is described below.
$ npm install @badeggg/mock-api
- Add fake-services folder on the root of your project.
$ cd /your/project/root $ mkdir fake-services
- Create series of folders relative to api path in fake-services.
Path parameters can be handled.
$ cd fake-services $ mkdir -p mocking/api/path
- Create text file as response entity.
$ cd mocking/api/path $ echo 'data to response' > response
- Now you can start the mock server to check what you just configured.
# start by command $ npx mock >> Fri Oct 15 2021 17:32:31 GMT+0800 (China Standard Time) INFO Mock-api listening on: 3000
// or start by js const mock = require('@badeggg/mock-api'); mock().then(server => { console.log('http mocking location: ', server.getHttpLocation()); // http mocking location: http://localhost:3000 console.log('websocket mocking location: ', server.getWsLocation()); // websocket mocking location: ws://localhost:3000 console.log('mocking on port: ', server.address().port); // mocking on port: 3000 });
# in another shell $ curl localhost:3000/mocking/api/path >> data to response
If your project is a nodejs back-end project, you may have figured out what else to do. Just edit the scripts section in package.json to make sure mock server is started and api requests are passed to mock server when developing the project.
If your project is general back-end project, a java project for example. You need install nodejs npm and do the similar things as a nodejs project. Welcome to javascript world ;-)
I'll do a rather detailed description here for common front-end projects. In a common front-end project, you need some coordination:
- Add a script in package.json, such that start the mock service first and then start the
well-known webpack dev-server when run this script.
- Of course the 'dev-server' part is dependent on your specific project.
- For example, if your project is constructed with vue-cli-service,
"serve-mock": "mock | vue-cli-service serve",
should be added
- Modify the original development start script, so that .mockingLocation file is removed first and
then start the well-known webpack dev-server when run this non-mocking start development script.
- Of course the 'dev-server' part is dependent on your specific project.
- For example, if your project is constructed with vue-cli-service,
"serve": "rm -f .mockingLocation && vue-cli-service serve",
should be the modified version script.
- Make sure the original dev-server api proxy is configured to mocking location when you are
mocking.
- For example, if your project is constructed with vue-cli-service, part of the vue.config.js
should looks like:
let MOCKING_LOCATION = null; let MOCKING_LOCATION_WS = null; if (fs.existsSync('./.mockingLocation')) { MOCKING_LOCATION = fs.readFileSync('./.mockingLocation', 'utf-8'); MOCKING_LOCATION_WS = MOCKING_LOCATION.replace('http', 'ws'); } module.exports = { devServer: { proxy: { '/api': MOCKING_LOCATION ? MOCKING_LOCATION : 'https://your.test.environment.com', '/websocket': { target: MOCKING_LOCATION_WS ? MOCKING_LOCATION_WS : 'wss://your.test.environment.com', ws: true }, }, }, };
- For example, if your project is constructed with vue-cli-service, part of the vue.config.js
should looks like:
For http, to configure different response for different request on same api path, you will need set the map file.
For websocket, you write a ws-response.js file in corresponding directory. Check websocket.
After proxy all api requests to mock server, you don't really need configure all of the api mocking. Check proxy 404 feature.
- Using in a common vue project: mock-api-example-vue
The map file is used to configure how to respond an http api request with current path. We say 'current path' means the directory where the map file is in and the api request path matchs.
The map file is optional. If this map file does not exist, contents in './response' will be sent to client. Lines starting with '#' will be ignored, any content after '#' will also be ignored. Each line is a map rule, elements in map rule are space-separated. Check config file common convention. 'Mock-api' will try to find a matching rule from the first line to the end line. A found matching rule will block any further search. If no matching rule is found, the default response file './response' will be used. If './response' file does not exist, a 404 is triggered, check proxy 404.
Syntax of the map rule is: [map word] [http method] [options]
. Here are few typical examples:
GET ./response # if the request method is GET, contents in ./response will be responsed
map -t 500 ./response # delay 500ms to response
'map' or 'MAP' appeared in the map rule start is trimmed. It is for good looking reason.
Case insensitive.
Http method may be configured to match request. If configured, it must be the first item in rule line(if 'map' word appears, http method must be the second item). If no method is configured, any method is considered matching.
Case insensitive.
Option names (e.g. --url-queries --status-code) are case sensitive.
No url query option configured in a map rule line means any query content of the request is ok with this map rule response. So with path parameter and body argument config and http method.
?<queries...>, -q <queries...>, --url-queries <queries...>
Form of <queries...> is almost like url query form. It contains one or more '&' seperated query config. Each query config has two parts and are connected by '='. The first part is query name, the second part is query value. If the query value config is surrounded by '{}', content between '{}' is regarded as a regular expression.
For example:
?name=badeggg&id={^\d+$}
-q name=badeggg
--url-queries name=badeggg
_<params...>, -p <params...>, --path-params <params...>
Form of <params...> is identical with form of <queries...>.
Unlike url queries, path params do not come with natural defination ---- after url path start
with '?' defines the url queries. To define path params, create a __parameterName__
folder in
corresponding position on 'series of folders'. This specific folder name should start and end
with double underscore while parameter name in the middle. Then in the map rule line in map
file, _params
will be _parameterName
. e.g: fake-services/some/path/__id__
define a 'id'
path parameter in api, map rule line map _id=123 ./response
in
fake-services/some/path/__id__/map
will match a request with path /some/path/123
.
For example:
_name=badeggg&id={^\d+$}
-p name=badeggg
--path-params name=badeggg
+<args...>, -a <args...>, --body-args <args...>
We can parse "application/json" and "application/x-www-form-urlencoded" type body of request to do the matching.
Form of <args...> is a superset of <queries...>. Besides the basic form of <queries...>, body
args also support deep property chain match ---- that means if the parsed body of request is
a multiple level js object e.g {data: {person: {name: 'badeggg'}}}
, the match rule line
+data.person.name=badeggg
will match the request. Actually,the specified body argument 'key'
(data.person.name
in the last example) is passed to js eval()
properly to get value of
the 'key' to do the matching. So mixing array with object or pure array in parsed body of
request works fine.
For example:
+data.person.name=badeggg
-a id={^\d+$}
--body-args country=china
-h <headers...>, --res-headers <headers...>
For example:
-h 'a-casual-header: a casual header' 'another-header: header value'
--res-headers 'content-type: image/jpeg'
To set multiple headers, either write one option name(-h) trailing with multiple header pairs or write multiple times option name(-h) trailing with header pair.
-c <code>, --status-code <code>
For example:
-c 200
--status-code 404
-m <method>, --req-method <method>
In most cases, you do not need formally set request method option, since the quick http method is more convenient.
Option value is case insensitive and will override quick http method setting.
For example:
-m get
--req-method post
-f <file>, --res-file-path <file>
Relative to directory of current map file or absolute path ---- although absolute path is not recommended, since we want 'fake-services' folder be tracked by version control software like git.
Since response file setting is basic and frequent. We have some convenient way to set it.
- quick path setting
-
In a map rule line, an item is assumed to be a response-path-item if
- it is just after the last quick url-query(starts with ?) / body-args(starts with +) / path-params(starts with _) and starts with ./
- or it is the last item and starts with ./
The condition-1-item found stop the finding of the condition-2-item.
Notice that quick path setting must start with ./ restrict it can't be an absolute path.
-
- implicit path
- if the map file does not exist or non map rule line match the request, 'fake-services/api/path/response' will be regarded as response file path
- if a map rule line does not specify a file path, './response' will be regarded as response file path
If the specified file has '.json' extension or has no extension, file content will be parsed and validated as json. If parse failed, a header 'Mock-Not-Validated-Json-File: {filePath}' will be set and responded. If the file is too big, will refuse to parse and a header 'Mock-Not-Validated-Json-File: filePath' will be set and responded. If parse succeeded or refused, a header 'Content-Type: application/json; charset=UTF-8' will be set and responded.
If the specified file has '.js' extension and --res-js-result option is set, file content will be evaluated as js script. If the script export a function, function executing result will be responded, else the script export will be responded. Check respond js result for details.
File content is always sent by expressJs sendFile except the specified file has '.js' extension and --res-js-result option is set.
For example:
-f ./response.json
--res-file-path ./picture.jpg
-t <time>, --delay-time <time>
Time unit should be 'ms'(millisecond) or 's'(second). An empty unit specifying means millisecond unit.
For example:
-t 500 # delay 500 milliseconds to respond
-t 500ms # delay 500 milliseconds to respond
--delay-time 1s # delay 1 second to respond
-r, --res-js-result
Whether evaluate the response js file and respond the result. The specifed response file path must have '.js' extension and export a function, an plain object, an array, a js primitive value or a binary type object (Buffer, ArrayBuffer, TypedArray, DataView).
This feature is useful when you 1) need mock a big json response and are tired of json syntax fixing line by line, e.g. ' --> "; 2) need some simple logic to generate response by request info.
If the file exports a function, the function will be executed with argument 'request', function returned value will be responded. The argument is a js object with request info:
{
method: req.method, // request http method
query: req.query, // request url query
params: req.params, // request path params
body: req.body, // request body
}
If the file does not export a function, the exported value will be responded.
Notice that, if this option is not set, file content will be responded no matter the file has '.js' extension or not.
If the executed result is a binary type object, it is normalized to 'Buffer' type before express send. Code detail.
There is buffer recover issue too in executing response.js
like
in executing ws-response.js
. To escape buffer recover, surround the result in a meta box
and set metaBox.responseEscapeBufferRecover
to true.
For example:
# map rule line
-r ./response-big-json.js
--res-js-result ./response-simple-logic.js
--res-js-result ./response-binary.js
--res-js-result ./response-escape-buffer-recover.js
# in ./response-big-json.js
module.exports = {
name: 'badeggg', // I am sick of json syntax fixing
age: 18,
// lots of fields
};
# in ./response-simple-logic.js
const echo = function(req) {
return req;
};
module.exports = echo;
# in ./response-binary.js
module.exports = () => {
return Buffer.from([1,2,3]); // client will receiver <Buffer 01 02 03>
};
# in ./response-escape-buffer-recover.js
module.exports = {
isMetaBox: true,
response: Buffer.from([1,2,3]), // client will receiver {"type":"Buffer","data":[1,2,3]}
responseEscapeBufferRecover: true,
};
You may not want to mock all of the api requests. When there is not a response configuration for the api request -- we say 404, which may caused by one of the following reasons:
- the corresponding path does not exist
- the corresponding path is turned off
- the map rules do not match for http
- 'ws-response.js' does not exist for websocket
'Mock-api' will proxy the request to the location specified by /your/project/root/fake-services/proxy404. e.g.:
$ echo 'https://nodejs.org' > /your/project/root/fake-services/proxy404
After the above command execution, all no-mocking-configuration requests will be proxied to https://nodejs.org.
In some cases, a single proxy 404 rule can not satisfy your requirement. You may configure
multiple proxy 404 destinations in proxy404
file.
In proxy404 file, lines starting with '#' will be ignored, any content after '#' will also be ignored. Each line is a proxy404 rule, elements in proxy404 rule are space-separated. Check config file common convention.
Syntax of the proxy 404 rule is: [http | ws | HTTP | WS] [match regexp] destination
If [http | ws | HTTP | WS]
is not set in a rule, it is regarded as an http rule. If
match regexp
is not set in a rule, the regular expression is implicitly set to .*
, that
means any path. The destination is a rule must be a valid url(new URL('destination')
is used
to validate).
Configuration lines are order sensitive. 'Mock-api' will try to find a matching rule from the first line to the end line. A found matching rule will block any further search. If no matching rule is found, 404 is then responded.
e.g.:
# Contents of /your/project/root/fake-services/proxy404.
#
# Configuration in this file will have the effect:
# 1) any no-mocking-configuration request whose path has 'baidu' text will be proxyed to https://baidu.com;
# 2) any no-mocking-configuration request whose path has 'bing' text will be proxyed to https://bing.com;
# 3) any no-mocking-configuration websocket request will be proxied to wss://demo.piesocket.com;
baidu https://baidu.com
bing https://bing.com
ws wss://demo.piesocket.com
To configure a websocket mocking service, write a js file named ws-response.js
in
corresponding directory. ws-response.js
exports websocket response or exports a function
which returns websocket response. A simple example:
// in /your/project/root/fake-services/ws/path/ws-response.js
module.exports = 'hi';
After above file is set, a websocket client connect to /ws/path
will receive 'hi' when
connection is open and when a message is sent to server ---- we say 'a trigger'. There are
three types of trigger:
WS-OPEN
, when websocket is openedWS-MESSAGE
, when a new message is receivedSELF-TRIGGER
, defined by a previous response meta box
A fixed 'hi' message is a little bit boring and not useful. More common cases are
ws-response.js
exporting a function which generate response at will.
The exporting function receives one argument triggerInfo
, which contains infomation you may
need to generate response.
triggerInfo
:
triggerName
{ 'WS-OPEN' | 'WS-MESSAGE' | 'SELF-TRIGGER' }currentMessage
{ null | String | Buffer }
Message received from client.currentMessageIsBinary
{ Boolean }request
{ http.IncomingMessage(pruned) }
The client HTTP GET request ---- pruned to contains only properties:complete
headers
httpVersion
method
rawHeaders
rawTrailers
trailers
url
query
{ Object }params
{ Object }
Path params. Defining path params for websocket is identical with defining path params for http.lineageArg
Infomation that a self trigger want pass to triggering. Type is dependent on self trigger specification. Check self trigger.lineageArgEscapeBufferRecover
{ Boolean }
Returned value of the exporting function is either a direct response or a meta box which surround the response. A meta box is used to specify a flexible response behavior.
- By default,
send
action is used for all responses. - A false response will not be sent by default ---- Boolean(response) to validate. Set
metaBox.insistSendEmpty
to send false response. - Any binary type object (Buffer, ArrayBuffer, TypedArray, DataView) is normalized to 'Buffer' type. Code detail.
Rules of a surrounding meta box:
- A meta box must be an object with
isMetaBox
property set. - Properties of a meta box:
isMetaBox
{ Boolean }
Must be set true to define a meta box.response
Response to websocket client. Rules of a direct response apply to this property when action is 'SEND'. When action is 'PING' or 'PONG', response is ping data or pong data. When action is 'CLOSE', response should be empty or an object withcode
andreason
properties which to be used with close.responseEscapeBufferRecover
{ Boolean }
Usually you just leave it alone.
Check base design of websocket mocking for details.insistSendEmpty
: { Boolean }
Usually you just leave it alone.
Recall that a false response is not sent to client by default. You may setinsistSendEmpty
to change that behavior.actionDelay
: { Number | String } Delay to act. Default is 0. e.g.:500 // 500 ms delay '1000ms' // 1000 ms delay '1.5s' // 1.5s delay
action
{ 'SEND' | 'PING' | 'PONG' | 'CLOSE' | 'send' | 'ping' | 'pong' | 'close' }
Response action. Default is 'SEND'.selfTrigger
{ Object | Object[] }
Check self trigger.
The rules of returned value of function apply to fixed exporting result.
ws-response.js
can be triggered by self. This is useful when you want specify autonomous
response to client. To do so, set returned metaBox.selfTrigger
property with an object
value. A self trigger object may have properties:
triggerDelay
{ Number | String }
Optional. e.g.:500 // 500 ms delay '1000ms' // 1000 ms delay '1.5s' // 1.5s delay
lineageArg
Optional. Infomation that a self trigger want pass to triggering. Any binary type object (Buffer, ArrayBuffer, TypedArray, DataView) is normalized to 'Buffer' type. Code detail.lineageArgEscapeBufferRecover
Usually you just leave it alone.
Check base design of websocket mocking for details.
If you just want a self trigger with no property decoration, set selfTrigger
with {}
. A
value like true
or 1
will not work.
If you want multiple self trigger in a single returned meta box, set selfTrigger
with an
array of trigger object.
Websocket mocking use the same port as http mocking.
ws-response.js
is executed in a child process ---- so as the specified 'http-response.js'
for http connections. We do this
- to avoid any potential disturbing to 'mock-api' main process
- to make sure every change of the file will take effect immediately.
Every new websocket connection starts a new
child process to execute ws-response.js
, and the child process live until connection down.
So change of ws-response.js
does not effect until next connection. And you may save some
information for use during a connection life.
There is a JSON.stringify / JSON.parse phase when passing ws-response.js
result back to main
process from child process. A 'Buffer' type value is turned to an object like
{ type: 'Buffer', data: [ 1, 2, 3 ] }
after this phase. 'Mock-api' automatically recover it
to a Buffer. This effect response
, metaBox.response
, metaBox.selfTrigger.lineageArg
and metaBox.selfTrigger[index].lineageArg
. If you fortunately want these properties be
object like { type: 'Buffer', data: [ 1, 2, 3 ] }
, you need set
metaBox.responseEscapeBufferRecover
, metaBox.selfTrigger.lineageArgEscapeBufferRecover
or
metaBox.selfTrigger[index].lineageArgEscapeBufferRecover
to true.
In some cases you may want to temporarily disable part of the mocking ---- e.g. try only two or three apis on test environment while the other apis on test environment are not ok yet. Create a file named 'off' or 'OFF' on some sub folder of fake-services, the folders that containing 'off' file and all of the sub folders will be considered 404, therefore the corresponding request will be proxied as 'proxy404' config. For example:
$ cd /your/project/root/fake-services/some/mock/path
$ touch off
After the above commands execution, any request whose path has prefix of /some/mock/path
will be considered as a no-mocking-configuration request and will be proxied as 'proxy404'
file config.
Notice that an 'off' file in the very fake-services folder will disable all the mockings. This is convenient when you want to use full real api services then use mocking services, back and forth.
Most of the config files(the map file, the proxy404 file for instances) of mock-api have the convention:
-
the basic config unit is a line of text
-
any content after #(pound sign) in a line is comment, which will be ignored
-
a backslash in the last character of a line will cause the next line(if any) concated to current line.
-
elements in a config unit(a line) is separated by one or more white space character(s), including space, tab, form feed, line feed, and other Unicode spaces
-
'pair chars' can help to set special characters in an item. Supported pair chars includes:
' pair to self
" pair to self
( pair to )
e.g.:
'should be together' should be separated 'multiple spaces in pair chars are reserved' 'half pair char" in another pair chars' is "ignored
The elements in a config unit may have different meaning for different config purpose.
- occasionally, npm script like
mock | node ./index.js
may not work, sincemock
is ready('.mockingLocation' file is set) beforenode ./index.js
start executing is not guaranteed. One of the solutions is to start mock-api in another terminal tab and then start your project. Separate starting also let you view mock-api logs in mock-api terminal tab, which is beneficial for debugging proxy problems related to mock-api. console.log
can be used to debug response.js / ws-response.js, but mock-api need be started in a separated terminal tab to view the log.- a response header 'From-Mocking-Fake-Service' was added if the response is from mocking
- touch an 'off' file in the very fake-services filder to turn off all mocking and use full real api services, remove it to back using mock. Check disable part of the mocking
- a very long config line can be separated to multiple lines with a trailing backslash(\)
- coordination with vue-cli-service
- coordination with create-react-app
- log file
- more configurable