Now with Playwright support!
If you write tests using playwright browser engine, and you want to mock your network responses easily – probably yes.
import teremock from 'teremock'
await teremock.start({ page })
// async stuff which is making requests, including redirects
First, teremock
intercept request (all xhr/fetch requests by default). Then, it looks for the mock file. If mock file exist, you get response from it. If not, request goes to the real backend.
Second, teremock
intercepts all responds, and writes them to the filesystem as mock files,
Sometimes it is more convenient to set mocks right in tests, without storing them to the file system. For that cases mocker.add method exist.
Example:
mocker.add(interceptor)
After that line, all request, matched interceptor
, will be mocked with interceptor.response
.
Note: dynamically added interceptors have priority over statically added interceptors. Also, latter
mocker.add
interceptors have higher priority. See interceptor below.
mocker.start(options)
All options are optional (that's why they called so), except page
.
const options = {
// Playwright option
page: page,
// Express options
app: express(),
env: { myApi: 'http://example.com/api' },
// Common options
// Named list of request interceptors
interceptors: {
testpage: {
url: 'localhost',
pass: true
},
example_com_api: {
url: 'example.com',
methods: `get,post`,
},
my_another_api: {
url: '/path/to/my/api',
response: async (request) => ({
status: 200,
headers: {},
body: request.query
})
},
option_requests: {
methods: 'option',
response: {
headers: {
'allow-origin': '*'
}
}
},
},
// Absolute path to working directory, where you want to store mocks
// path.resolve(process.cwd(), '__teremocks__') by default
wd: path.resolve(__dirname, '__teremocks__'),
// Run as CI if true. In CI mode any non-passable request will not go to the real backend
// Default is `process.env.CI` value
ci: false,
// Extends values for any mocked response
// You could redefine any response property, including headers, body and ttfb
responseOverrides: {
// A sequence of ttfb values
// Each new request will get the next (looped) ttfb value
// Could be usefull when find flaky tests and race conditions
// In this example, all requests (first 10) will work as a stack, not as a queue
ttfb: [900, 800, 700, 600, 500, 400, 300, 200, 100, 0]
},
onStop: ({ matched }) => {
// `matched` contains data about all urls and matched interceptors since last `start()`
// example: Map(1) { interceptorName => 'http://localhost:3000/api?q=x' }
}
}
Interceptor – is a big conception in teremock
. Interceptor is an object, which have two different groups of properties:
- Matcher group: these properties determine, whether to intercept particular request, or not.
- Provider group: what to do with request: 1) pass to real backend 2) respond with inline resoponse 3) try to find response mock on file system.
Interceptor is used, if all matchers are matched against request. So, if one matcher is not matched, given interceptor will not be used.
It is recommended to have interceptors for all possible (for your test app) requests. All non-covered requests will be aborted.
A string, which defines urls to match. It works when request.url.includes(url) === true
– so, ample.com
will match both http://exampe.com
and https://not-example.com/api?foo-bar
. Note: prefer not to place query params to urls, because they could be randomly sorted in real request.
Default value: *
.
Comma-separated list of playwright request resource types. By default, only xhr
and fetch
request are mockable, but there are many situation where you may want to mock html documents, js files and, for example, the whole page of the facebook auth.
Default value: xhr,fetch
.
Unsorted one-level object of query params. Missing params will not affect the interception. Example: { foo: 'bar' }
will match <url>?foo=bar
as well as <url>?alice=bob&foo=bar&baz=1
.
Duplicated values (e.g. ?foo=bar&foo=baz
) are not supported.
Default value: {}
.
Comma-separated list of http methods to be matched. Example: get,post,put
.
Default value: *
.
Unsorted one-level ignore-cased object of request headers.
Default value: {}
.
Unsorted deep object to match request body.
Default value: {}
.
If true
, pass request to the real backend. It is not recommended to pass any request outside of your app, since your tests became unstable and dependent from backends stability and network availability.
Default value: false
.
If present, it is used instead of file-based mocks. Usefull for testing different responses for the same request, and/or for mocking big-sized data.
Functional interceptor response must be async (return Promise), which must resolve with full response object (e.g. { headers: {}, status: 200, body: ... }
). Use functional interceptors when you need dynamic behaviour (e.g. translate random cookie to random GET param). Dont use functional interceptors for handling too many different requests or as a router between mocks.
Default value: null
.
See Change naming rules below.
The name of mock file is consist of five parts:
Name will be taken from interceptor.name, or from the interceptor key, or from the hostname+path of request.url.
e.g. post
If request method is get
, and query is short enough, it is used. Otherwise, three words are used. These words are pseudorandom, and depends on a) request url (without query) b) query params (sorted, deduped) c) body params (sorted).
@todo examples
In many cases it is important to be independent from some query and body params, which, for example, have random value for each requests (e.g. timestamp). There are four different list for skipping some parameters, when calculating mock filename: whitelist and blacklist for query and body parameters.
For example, you want to skip timestamp
GET-parameter, randomToken
and nested data.wuid
POST-parameters. Then, you need to construct two lists, and set them to mocker options:
const dynamicQueryParams = [
'timestamp'
]
const dynamicBodyParams = [
'randomToken',
['data', 'wuid']
]
const interceptor = {
naming: {
query: {
blacklist: dynamicQueryParams
},
body: {
blacklist: dynamicBodyParams
}
}
})
Now, when you have a POST request with url and body:
http://example.com/?foo=bar×tamp=123
{
randomToken: 'qweasd',
data: {
alice: 'bob',
wuid: 32
}
}
the mock filename will be exact as if it were just
http://example.com/?foo=bar
{
data: {
alice: 'bob'
}
}
It is not recommended to use whitelist
, because you may encounting mocks filenames collision. But in some rare cases (for example, when some keys are random) whitelist
could be usefull.
It is not possible to use different lists for different urls simultaneously, but if you really need that, just create an issue!
Starts the mocker. Mocks for all requests matched options.capture
will be used, but no mocks used before mocker.start()
and after mocker.stop()
Both mocker.start()
and mocker.stop()
return a Promise
.
Stops teremock, removes all handlers from playwright, disables request interception.
Adds new interceptor to teremock in the middle of a test. Interceptor have priority over interceptors
, defined in teremock.start(). To remove it, just call function which was returned:
const remove404 = mocker.add({ url: 'example.com', response: { status: 404 } })
// ... request for 'example.com' → check 404 behaviour here
remove404()
const remove500 = mocker.add({ url: 'example.com', response: { status: 500 } })
// .. same request for 'example.com' → check 500 behaviour here
remove500()
Note: in most cases you dont need dynamic interceptors – it is better to write two different tests.
Example:
const spy = mocker.spy({ url: 'example.com', query: { id: '123' } })
await stuff()
expect(spy.calledOnce).toBe(true)
spy.dismiss()
teremock
uses debug
library for hidden logs, and signale
library for warning and error logs.
Warnings and errors are printed unconditionally (all the time).
Debug logs are hidden, but could be switched on with enviroment variable DEBUG
with valies, started with teremock...
. For example: DEBUG=teremock* yarn jest
.
Use DEBUG=teremock:trace ...
for tracing your request. Sample output:
teremock:trace http://localhost:3000/api?foo=bar&baz=1 ← page.on('request') fired +0ms
teremock:trace http://localhost:3000/api?foo=bar&baz=1 ← handling request +0ms
teremock:trace http://localhost:3000/api?foo=bar&baz=1 ← mock not found in storage +1ms
teremock:trace http://localhost:3000/api?foo=bar&baz=1 ← request.continue() +0ms
teremock:trace http://localhost:3000/api?foo=bar&baz=1 → page.on('response') fired +37ms
teremock:trace http://localhost:3000/api?foo=bar&baz=1 → handling response +53ms
teremock:trace http://localhost:3000/api?foo=bar&baz=1 → storing mock basic--get-foo-bar +1ms
teremock:trace http://localhost:3000/api?foo=bar&baz=1 → finish +1ms
in progress
This type of mockers (which teremock belongs to) could mock any client-side request, including xhr, fetch, script, images, redirect pages. But 1) cannot mock websockets 2) cannot mock server-side request 3) cannot mock initial request of new page.
- Teremock
This type of mockers have one fundamental limitation: you have to be in the same nodejs process. This type of mockers wont work for you if your app runs in a separate process (including child process), than your testing framework.
- Nock ← simple and powerfull mocker, which decorates native http.request and http.ClientRequest functions.
This type of mockers allows you to test your system very close to the real network topology, but only if you could configure all your endpoints (change real urls to your mocked endpoints).
- MockIt ← allows fastly create stub endpoints. It is better in cases when you have isomorphic applications, and you have requests both – on server and on client-side.