Skip to main content

Request Mocks and Spies

WebdriverIO comes with built-in support for modifying network responses that allows you to focus testing your frontend application without having to setup your backend or a mock server. You can define custom responses for web resources like REST API requests in your test and modify them dynamically.

info

Note that using the mock command requires support for WebDriver Bidi. That is usually the case when running tests locally in a Chromium based browser or on Firefox as well as if you use a Selenium Grid v4 or higher. If you run tests in the cloud, make sure that your cloud provider supports WebDriver Bidi.

Creating a mock​

Before you can modify any responses you have define a mock first. This mock is described by the resource url and can be filtered by the request method or headers. The resource is matched using a URLPattern, where * matches any sequence of characters. A url without a protocol is matched against the path of the request only, so */users/list matches that path on any origin:

// mock all resources ending with "/users/list"
const userListMock = await browser.mock('*/users/list')

// or you can specify the mock by filtering resources by headers or
// status code, only mock successful requests to json resources
const strictMock = await browser.mock('*', {
// mock all json responses
requestHeaders: { 'Content-Type': 'application/json' },
// that were successful
statusCode: 200
})

// instead of a string you can also pass in a `URLPattern`; the polyfill
// also works in runtimes without native URLPattern support
import { URLPattern } from 'urlpattern-polyfill'
const patternMock = await browser.mock(new URLPattern({ pathname: '/users/list' }))
warning

Use a single * for URL wildcards; it also matches /. Consecutive wildcards before fixed text, such as **/api/** or **/data.json, can cause excessive regex backtracking on unrelated URLs and freeze a test. See issue #13548. In component tests, also use a fixed protocol and hostname to keep runner traffic outside the intercept; see component testing request mocks.

Specifying custom responses​

Once you have defined a mock you can define custom responses for it. Those custom responses can be either an object to respond a JSON, a local file to respond with a custom fixture or a web resource to replace the response with a resource from the internet.

Mocking API Requests​

In order to mock API requests where you expect a JSON response all you need to do is to call respond on the mock object with an arbitrary object you want to return, e.g.:

const mock = await browser.mock('https://todo-backend-express-knex.herokuapp.com/')

mock.respond([{
title: 'Injected (non) completed Todo',
order: null,
completed: false
}, {
title: 'Injected completed Todo',
order: null,
completed: true
}], {
headers: {
'Access-Control-Allow-Origin': '*'
},
fetchResponse: false
})

await browser.url('https://todobackend.com/client/index.html?https://todo-backend-express-knex.herokuapp.com/')

await $('#todo-list li').waitForExist()
console.log(await $$('#todo-list li').map(el => el.getText()))
// outputs: "[ 'Injected (non) completed Todo', 'Injected completed Todo' ]"

You can also modify the response headers as well as the status code by passing in some mock response params as follows:

mock.respond({ ... }, {
// respond with status code 404
statusCode: 404,
// merge response headers with following headers
headers: { 'x-custom-header': 'foobar' }
})

If you want the mock not to call the backend at all, you can pass false for the fetchResponse flag.

mock.respond({ ... }, {
// do not call the actual backend
fetchResponse: false
})

fetchResponse: false never calls the backend. A mock created with a statusCode or responseHeaders filter needs that response to decide whether it matches, so respond() and respondOnce() throw if you combine them. Drop the response filter, or leave fetchResponse unset so the mock can read the backend response and then replace it.

It is recommend to store custom responses in fixture files so you can just require them in your test as follows:

// requires Node.js v16.14.0 or higher to support JSON import assertions
import responseFixture from './__fixtures__/apiResponse.json' assert { type: 'json' }
mock.respond(responseFixture)

Mocking text resources​

If you like to modify text resources like JavaScript, CSS files or other text based resources you can just pass in a file path and WebdriverIO will replaces the original resource with it, e.g.:

const scriptMock = await browser.mock('*/script.min.js')
scriptMock.respond('./tests/fixtures/script.js')

// or respond with your custom JS
scriptMock.respond('alert("I am a mocked resource")')

Redirect web resources​

You can also just replace a web resource with another web resource if your desired response is already hosted on the web. This works with individual page resources as well as with a webpage itself, e.g.:

const pageMock = await browser.mock('https://google.com/')
await pageMock.respond('https://webdriver.io')
await browser.url('https://google.com')
console.log(await browser.getTitle()) // returns "WebdriverIO · Next-gen browser and mobile automation test framework for Node.js"

Dynamic responses​

If your mock response depends on the original resource response you can also dynamically modify the resource by passing in a function receives the original response as parameter and sets the mock based on the return value, e.g.:

const mock = await browser.mock('https://todo-backend-express-knex.herokuapp.com/', {
method: 'get'
})

mock.respond((req) => {
// replace todo content with their list number
return req.body.map((item, i) => ({ ...item, title: i }))
})

await browser.url('https://todobackend.com/client/index.html?https://todo-backend-express-knex.herokuapp.com/')

await $('#todo-list li').waitForExist()
console.log(await $$('#todo-list li label').map((el) => el.getText()))
// returns
// [
// '0', '1', '2', '19', '20',
// '21', '3', '4', '5', '6',
// '7', '8', '9', '10', '11',
// '12', '13', '14', '15', '16',
// '17', '18', '22'
// ]

Aborting mocks​

Instead of returning a custom response you can also just abort the request with one of the following HTTP errors:

  • Failed
  • Aborted
  • TimedOut
  • AccessDenied
  • ConnectionClosed
  • ConnectionReset
  • ConnectionRefused
  • ConnectionAborted
  • ConnectionFailed
  • NameNotResolved
  • InternetDisconnected
  • AddressUnreachable
  • BlockedByClient
  • BlockedByResponse

This is very useful if you want to block 3rd party script from your page that have a negative influence on your functional test. You can abort a mock by just calling abort or abortOnce, e.g.:

const mock = await browser.mock('https://www.google-analytics.com/*')
mock.abort('Failed')

Spies​

Every mock is automatically a spy that counts the amount of requests the browser made to that resource. If you don't apply a custom response or abort reason to the mock it continues with the default response you would normally receive. This allows you to check how many times the browser made the request, e.g. to a certain API endpoint.

const mock = await browser.mock('*/user', { method: 'post' })
console.log(mock.calls.length) // returns 0

// register user
await $('#username').setValue('randomUser')
await $('password').setValue('password123')
await $('password_repeat').setValue('password123')
await $('button[type="submit"]').click()

// check if API request was made
expect(mock.calls.length).toBe(1)

// assert response
expect(mock.calls[0].body).toEqual({ success: true })

If you need to wait until a matching request has responded, use mock.waitForResponse(options). See the API reference: waitForResponse.

Multi-remote​

On a multi-remote browser, mock() returns a MultiRemoteMock rather than one Mock. Methods such as respond() and restore() run on every instance. waitForResponse() waits until every instance has a matching response. Captured requests stay on the mock for that browser:

const mock = await browser.mock('*/user', { method: 'post' })
mock.respond({ success: true })

// register a user in every browser so each session sends the request
await browser.$('#username').setValue('randomUser')
await browser.$('#password').setValue('password123')
await browser.$('#password_repeat').setValue('password123')
await browser.$('button[type="submit"]').click()

await mock.waitForResponse()

expect(mock.getInstance('myChromeBrowser').calls).toHaveLength(1)
expect(mock.getInstance('myFirefoxBrowser').calls).toHaveLength(1)

mock.instances lists those names in the order the mocks were created. getInstance throws Multi-remote object has no instance named "<name>" when the name is not in that list. A mock created from browser.select('myFirefoxBrowser', 'myChromeBrowser') lists Firefox first, which can differ from browser.instances.

To stub one browser only, call mock() on that instance:

const chromeOnly = await browser.getInstance('myChromeBrowser').mock('*/user')

Welcome! How can I help?

WebdriverIO AI Copilot