- Automatically record new HTTP(s) requests.
- Replay recordings when testing.
- Customize responses.
- Works well with supertest.
- Predictable, deterministic filepaths.
- Normalize the
request
&response
. - Associate session-based cookies & OAuth tokens to users.
- Ignore requests you don't want to record.
$ yarn add node-recorder --dev
# or
$ npm install node-recorder --save-dev
- By simply including
node-recorder
, all HTTP(s) requests are intercepted. - By default,
RECORD
mode records new recordings, and replays existing fixures. - When in
NODE_ENV=test
orCI=true
,REPLAY
mode replays existing recordings, and throws an error when one doesn't exist. (So that local tests don't suddenly fail in CI)
bypass
- All network requests bypass the recorder and respond as usual.record
- Record only new network requests (i.e. those without recordings), while replaying existing recordings.replay
- Replay all network requests using recordings. If a recording is missing, an error is thrown.rerecord
- Re-record all network requests.
$ node -r node-recorder path/to/server.js
(This also works with mocha
!)
$ RECORDER=ignore node -r node-recorder path/to/server.js
Included is a jest-preset
that will automatically include node-recorder
and a custom plugin to make toggling modes easier.
// jest.config.js
module.exports = {
preset: "node-recorder/jest-preset"
};
Now, running jest --watch
will add a new r
option:
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press r to change recording mode from "REPLAY".
› Press Enter to trigger a test run.
Pressing r
will toggle between the various modes:
╭─────────────────────────────╮
│ │
│ node-recorder: RECORD │
│ │
╰─────────────────────────────╯
Within your project, you can create a recorder.config.js
that exports:
// recorder.conig.js
module.exports = {
identify(request, response) {...},
ignore(request) {...},
normalize(request, response) {...}
}
request
is the same as the recording (e.g.body
,headers
,href
,method
), but with an additionalurl
property from https://github.com/unshiftio/url-parse to simplify conditional logic.response
containsbody
,headers
, &statusCode
.
This is useful when network requests are stateful, in that they rely on an authorization call first, then they pass along a token/cookie to subsequent calls:
- Suppose you login by calling
/login?user=foo&password=bar
. - The response contains
{ "token": "abc123" }3. Now, to get data, you call
/api?token=abc123`.
When recording recordings, the token abc123
isn't clearly associated with the user foo
.
To address this, you can identify
the request
and response
, so that the recordings are aliased accordingly:
identify(request, response) {
const { user, token } = request.query
if (request.href.endsWith("/login")) {
// We know the user, but not the token yet
if (!response) {
return user
}
// Upon login, associate this `user` with the `token`
return [user, response.body.token]
}
// API calls supply a `token`, which has been associated with a `user`
if (request.href.endsWith("/api")) {
return token
}
}
Now, when recorded recordings will look like:
127.0.0.1/login/${hash}.${user}.json
127.0.0.1/api/${hash}.${user}.json
This way, similar-looking network requests (e.g. login & GraphQL) can be differentiated and easily searched for.
Typically, you don't want to record recordings for things like analytics or reporting.
// recorder.conig.js
module.exports = {
ignore(request) {
if (request.href.includes("www.google-analytics.com")) {
return true;
}
return false;
}
};
Recordings are meant to make development & testing easier, so modification is necessary.
- Changing
request
changes the filenamehash
of the recording. You may need torecord
again. normalize
is called before the network request and after. This means thatresponse
may beundefined
!- You can change
response
by hand, or vianormalize
without affecting the filenamehash
of the recording.
module.exports = {
normalize(request, response) {
// Suppose you never care about `user-agent`
delete request.headers["user-agent"];
// We may not have a response (yet)
if (response) {
// ...or the `date`
delete response;
}
}
};