mockup-service
License
MIT
About
This module is designed to help with mocking network services some code to be tested relies on. It supports an extensible domain-specific language describing an ordered list of rules with each rule containing a test applied on incoming requests and a response to be sent back in case of test succeeds.
Install
npm install -D @cepharum/mockup-service
Usage
Consider the following definition of rules for an HTTP service:
headers.content-type = text/plain ==>
Hello tester!
EOT
headers.content-type = text/html AND method = POST ==>
Hello posting HTML-tester!
EOT
headers.content-type ~ /image\/jpe?g/ ==>
Hello JPEG-tester!
EOT
/just/some/path ==>
Hello path-tester!
EOT
/some/interpolated/response ==>
Hello {{headers.x-value}}-tester!
EOT
/api/:prefix/test ==>
Hello {{rule.params.prefix}}-tester!
EOT
/some/actual/message AND query.type === "actual" ==> MSG
set-cookie: mycookie=value:set
x-folded-value: so
me
folded value
Hello {{query.type}} message-tester!
EOM
In a unit test this definition could be used like this:
"use strict";
const { describe, before, after } = require( "mocha" );
const { HttpService } = require( "@cepharum/mockup-service" );
const Mock = new HttpService( `
headers.content-type = text/plain ==>
Hello tester!
EOT
headers.content-type = text/html AND method = POST ==>
Hello posting HTML-tester!
EOT
headers.content-type ~ /image\/jpe?g/ ==>
Hello JPEG-tester!
EOT
/just/some/path ==>
Hello path-tester!
EOT
/some/interpolated/response ==>
Hello {{headers.x-value}}-tester!
EOT
/api/:prefix/test ==>
Hello {{rule.params.prefix}}-tester!
EOT
/some/actual/message AND query.type === "actual" ==> MSG
set-cookie: mycookie=value:set
x-folded-value: so
me
folded value
Hello {{query.type}} message-tester!
EOM
` );
describe( "Mock-up service for HTTP", () => {
before( () => Mock.start() );
after( () => Mock.stop() );
it( "responds ...", () => {
// return require( "node-fetch" )( Mock.url ).then( response => {} );
} );
// TODO: add more tests here
} );
Using Mock.url
in actual tests to be added the mocked service's URL is available for use with your particular client library to be tested. Instead of using Mock.url
you might also use Mock.address
to fetch the IP address service is listening on and Mock.port
to get the related port service is listening on.
The service mock-up also includes a client Mock.query()
for querying the service but this mostly for testing the implementation of mock-up service itself.
API
The API basically consists of services to be instantiated in combination with a script describing how the resulting service is meant to respond to either incoming request.
As demonstrated before, in case of HTTP service this looks like this:
const { HttpService } = require( "@cepharum/mockup-service" );
const Mock = new HttpService( scriptDefiningRules );
Methods
Every such instance of a service provides the following methods:
Mock.start() : Promise<{address, port}>
Starts service for processing defined rules on every incoming request. The method promises information on then fully started service. This information is promised as object containing running service's IP address in property address
and its port in property port
.
Mock.stop() : Promise
Shuts down previously started service. Promises service shut down eventually.
Mock.query() : Promise
This method is provided to query the started service e.g. for testing whether some defined response is actually delivered. This is used a lot in testing this library. The method's signature stringly depends on type of service:
HttpService: Mock.query( method, url [, headers [, body] ] ) : Promise
This version accepts desired HTTP method and URL for request. In addition, an object listing request headers and some request payload might be given.
Properties
Mock.url
This property is exposing the service's URL. Its content strongly depends on type of service. This information is reliable after having started service, only.
Mock.address
This property is exposing the address the service is listening on. This information is reliable after having started service, only.
Mock.remoteAddress
This property is exposing the address for accessing the service from a client's point of view. This information is reliable after having started service, only.
Mock.port
This property is exposing the port the service is listening on. This information is reliable after having started service, only.
Events
Either service instance is deriving from EventEmitter
as provided by Node and thus it is possible to manage listeners for selected events to be emitted by either service.
request
The request
-event is emitted prior to searching defined rules for the earliest one matching incoming request. Any listener is invoked with these arguments:
- descriptor of incoming request
- manager for controlling response
response
The response
-event is emitted after having sent response to client. Any listener is invoked with these arguments:
- descriptor of incoming request
- manager for controlling response
- an array of
Buffer
instances sent in response - descriptor of matched rule which delivered the response
error
The error
-event is emitted when handling any request resulted in error. This might happen once per received request. Any listener is invoked with a description of error as instance of Error
as sole argument.
Syntax
The script for mocking-up a service is a sequence of rules processed from top to bottom. Every rule defines a test to be performed on any incoming request and a response to send back in case of a matching test.
<test> ==> <response>
Several kinds of tests are supported:
Tests
Custom Comparison Tests
Custom comparison tests consist of a source's name exposing some actual value, a comparing operator and a literal value to compare read source with.
<source> <operator> <value>
The source may consist of several segments separated by full stop with each segment selecting another level of previously addressed information while descending into a hierarchy of information. The initial source is description of current request. This description may be include different additional information depending on selected type of mock-up service.
The operator is one out of these options:
Operator | Comparison |
---|---|
= |
value equals source |
== |
value equals source |
!= |
value is different from source |
<> |
value is different from source |
<> |
value is different from source |
=== |
value equals source and is of same type |
eq |
value equals source and is of same type |
!== |
value is different from source or differs from source by type |
neq |
value is different from source or differs from source by type |
ne |
value is different from source or differs from source by type |
< |
value is less than source |
lt |
value is less than source |
<= |
value is less than or equal source |
lte |
value is less than or equal source |
> |
value is greater than source |
gt |
value is greater than source |
>= |
value is greater than or equal source |
gte |
value is greater than or equal source |
~ |
value is a regular expression that matches source |
!~ |
value is a regular expression that doesn't match source |
:::tip Example
path == /some/path
is testing path name of URL in context of an HTTP service. In context of same service query.arg = 1
tests whether URL query parameter named arg
has value 1
or not.
:::
See the following table for a list of probable sources:
Name | Description |
---|---|
method |
verb of HTTP request selecting request method |
httpVersion |
HTTP version selected by request |
headers.* |
HTTP request headers - Replace * with name of header to read. Headers are provided with all letters converted to lowercase variants. |
url |
full request URL consisting of path and query |
path |
leading part of request URL up to first ?
|
query.* |
query parameter extracted from part of request URL following first contained ? - Replace * with name of parameter to read. |
params.* |
parameter extracted from pathname of request URL (in opposition to parameters found in its query) - Works after using convenient path test as described below, only. Replace * with name of parameter to read |
body.* |
parameter extracted from transmitted body - Works if body is encoded as JSON and header "content-type" is set. Replace * with name of parameter to read. |
Conveniently Test Path
Tests starting with a forward slash are considered to provide a path name pattern to be matched by requests. The provided pattern is passed through path-to-regexp to generate a pattern that's eventually used to test an incoming request. See description of supported syntax there.
The pattern may include parameters to be exposed in request.params
. This enables use of those parameters when defining interpolated responses.
:::tip Example
/some/:myname*
is testing path name of URL in context of an HTTP service. Matching path names are /some
, /some/sub
, /some/deep/arbitrary
etc. with exposing part following /some/
as params.myname
e.g. for response interpolation.
:::
Combining Tests
Multiple tests may be combined per rule using either keyword AND
or OR
between two adjacent test definitions. Using either keyword result in an intersection or union of combined tests. Multiple tests may be combined this way, but only one of the keywords may be used throughout the combination of tests in a single rule.
:::tip Examples
Intersection: <test-a> AND <test-b>
or <test-a> AND <test-b> AND <test-c>
Union: <test-a> OR <test-b>
or <test-a> OR <test-b> OR <test-c>
:::
Responses
In a rule's definition response is separated from preceding tests by ==>
. This arrow must be written literally. Additional meta information on response might be given after that arrow, but in same line.
Actual response starts after arrow and some optional meta information. Either kind of response may use custom marker for end of response, but currently all supported response types use EOT
written in a separate line at least.
Delaying Responses
Responses may be delayed by intention. A delay is defined by writing number of milliseconds followed by another arrow ==>
right after the arrow marking end of tests as described before.
<test> ==> 1500 ==> <response>
This results in response being sent back to client in about 1500ms.
:::warning Due to Javascript lacking realtime capabilities either time may be considered estimate value of delay, only. :::
By defining a range using two positive integer values separated by a single dash the actual delay per response is picked randomly from this defined range.
<test> ==> 1500-5000 ==> <response>
This results in response being sent back to client with an arbitrary delay between 1500ms and 5000ms.
/just/some/path ==> 5000 ==>
Hello path-tester!
EOT
Selecting Type of Response
The type of response is selected using special type name right after ==>
. In case of declaring a delay this applies to the second ==>
. Following type names are supported:
Type Name | Resulting Type of Response |
---|---|
TEXT |
Simple Text |
MSG |
RFC-2822-Like Message |
MESSAGE |
RFC-2822-Like Message |
When omitting this information TEXT
is used by default.
:::warning Note:
This information selects one out of several supported response handlers. It doesn't define the kind of actual response but the way it is defined. Using TEXT
might be sufficient to sent arbitrary data, but MSG
is required to be actually able to define response headers. So, sending plain text responses with custom headers still requires use of MSG
response type.
:::
Simple Text Responses
Starting with line succeeding the one containing ==>
with optionally given meta information and type selector the actual response is provided as regular text. EOT
must be written in a separate line to mark end of text.
The text might be indented. Shallowest indentation common to all lines of response is ignored.
/api/:prefix/test ==>
Hello {{rule.params.prefix}}-tester!
EOT
RFC-2822-Like Messages
This type of response doesn't implicitly affect content of resulting response. However it enables support to separately define response headers and response payload in a format similar to RFC-2822. The meaning of response headers strongly depends on actually used service. In context of HTTP service the response headers are actual headers of a RFC-2822-compliant message sent back to client.
Starting with line succeeding the one containing ==>
with optionally given meta information and type selector the actual response is provided. Initial lines of response are defining response headers. Starting after first empty line of response the actual response payload is given. EOT
must be written in a separate line to mark end of text.
The response payload might be indented. Shallowest indentation common to all lines of response is ignored. Due to that this response type isn't suitable for sending binary data without proper transfer encoding.
/some/actual/message AND query.type === "actual" ==> MSG
set-cookie: mycookie=value:set
x-folded-value: so
me
folded value
Hello {{query.type}} message-tester!
EOM
Script Processing
For every incoming request the whole sequence of rules is processed. Iterating over defined rules top to bottom the first rule in sequence with matching test is picked to provide a response. Any succeeding rules are ignored for the rest of either request.