diff --git a/docs/README.md b/docs/README.md index 560b9493f15..5f99ca43e4c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Documentation -> This is a development version of the documentation. The functionality described here may not be included in the current release version. Unreleased functionality may change or be dropped before the next release. Documentation for the release version is available at the [TestCafe website](https://devexpress.github.io/testcafe/documentation/getting-started/). +> This is the documentation's development version. The functionality described here may not be included in the current release version. Unreleased functionality may change or be dropped before the next release. The release version's documentation is available at the [TestCafe website](https://devexpress.github.io/testcafe/documentation/getting-started/). * [GETTING STARTED](articles/documentation/getting-started/README.md) * [USING TESTCAFE](articles/documentation/using-testcafe/README.md) @@ -60,6 +60,7 @@ * [Assertion API](articles/documentation/test-api/assertions/assertion-api.md) * [Obtaining Data from the Client](articles/documentation/test-api/obtaining-data-from-the-client/README.md) * [Examples of Using Client Functions](articles/documentation/test-api/obtaining-data-from-the-client/examples-of-using-client-functions.md) + * [Intercepting and Mocking HTTP Requests](articles/documentation/test-api/intercepting-and-mocking-http-requests.md) * [Waiting for Page Elements to Appear](articles/documentation/test-api/waiting-for-page-elements-to-appear.md) * [Authentication](articles/documentation/test-api/authentication/README.md) * [User Roles](articles/documentation/test-api/authentication/user-roles.md) diff --git a/docs/articles/documentation/test-api/README.md b/docs/articles/documentation/test-api/README.md index 191c1ea2c1d..3b52ee6162c 100644 --- a/docs/articles/documentation/test-api/README.md +++ b/docs/articles/documentation/test-api/README.md @@ -20,6 +20,7 @@ The following topics describe the API used to manipulate the webpage and check i * [Selecting Page Elements](selecting-page-elements/README.md) * [Assertions](assertions/README.md) * [Obtaining Data From the Client](obtaining-data-from-the-client/README.md) +* [Intercepting and Mocking HTTP Requests](intercepting-and-mocking-http-requests.md) * [Waiting for Page Elements to Appear](waiting-for-page-elements-to-appear.md) * [Authentication](authentication/README.md) * [Pausing the Test](pausing-the-test.md) diff --git a/docs/articles/documentation/test-api/a-z-index.md b/docs/articles/documentation/test-api/a-z-index.md index 1afedc9eb45..4fc3d496a35 100644 --- a/docs/articles/documentation/test-api/a-z-index.md +++ b/docs/articles/documentation/test-api/a-z-index.md @@ -19,7 +19,16 @@ This topic lists test API members in alphabetical order. * [httpAuth](authentication/http-authentication.md) * [only](test-code-structure.md#skipping-tests) * [page](test-code-structure.md#specifying-the-start-webpage) + * [requestHooks](intercepting-and-mocking-http-requests.md#attaching-hooks-to-tests-and-fixtures) * [skip](test-code-structure.md#skipping-tests) +* [RequestLogger](intercepting-and-mocking-http-requests.md#logging-http-requests) + * [contains](intercepting-and-mocking-http-requests.md#methods) + * [count](intercepting-and-mocking-http-requests.md#methods) + * [clear](intercepting-and-mocking-http-requests.md#methods) + * [requests](intercepting-and-mocking-http-requests.md#properties) +* [RequestMock](intercepting-and-mocking-http-requests.md#mocking-http-responses) + * [onRequestTo](intercepting-and-mocking-http-requests.md#onrequestto) + * [respond](intercepting-and-mocking-http-requests.md#respond) * [Role](authentication/user-roles.md) * [anonymous](authentication/user-roles.md#anonymous-role) * [Selector](selecting-page-elements/selectors/creating-selectors.md) @@ -47,8 +56,10 @@ This topic lists test API members in alphabetical order. * [httpAuth](authentication/http-authentication.md) * [only](test-code-structure.md#skipping-tests) * [page](test-code-structure.md#specifying-the-start-webpage) + * [requestHooks](intercepting-and-mocking-http-requests.md#attaching-hooks-to-tests-and-fixtures) * [skip](test-code-structure.md#skipping-tests) * [Test Controller](test-code-structure.md#test-controller) + * [addRequestHooks](intercepting-and-mocking-http-requests.md#attaching-hooks-to-tests-and-fixtures) * [clearUpload](actions/upload.md#clear-file-upload-input) * [click](actions/click.md) * [ctx](test-code-structure.md#sharing-variables-between-test-hooks-and-test-code) @@ -81,6 +92,7 @@ This topic lists test API members in alphabetical order. * [maximizeWindow](actions/resize-window.md#maximizing-the-window) * [navigate](actions/navigate.md) * [pressKey](actions/press-key.md) + * [removeRequestHooks](intercepting-and-mocking-http-requests.md#attaching-hooks-to-tests-and-fixtures) * [resizeWindow](actions/resize-window.md#setting-the-window-size) * [resizeWindowToFitDevice](actions/resize-window.md#fitting-the-window-into-a-particular-device) * [rightClick](actions/right-click.md) diff --git a/docs/articles/documentation/test-api/intercepting-and-mocking-http-requests.md b/docs/articles/documentation/test-api/intercepting-and-mocking-http-requests.md new file mode 100644 index 00000000000..feead0d06c9 --- /dev/null +++ b/docs/articles/documentation/test-api/intercepting-and-mocking-http-requests.md @@ -0,0 +1,547 @@ +--- +layout: docs +title: Intercepting and Mocking HTTP Requests +permalink: /documentation/test-api/intercepting-and-mocking-http-requests.html +checked: true +--- +# Intercepting and Mocking HTTP Requests + +This topic describes how you can handle HTTP requests in your tests. TestCafe allows you to log them and mock the responses out of the box. You can also create a custom HTTP request hook, which allows you, for instance, to emulate Kerberos or client certificate authentication. + +* [Logging HTTP Requests](#logging-http-requests) +* [Mocking HTTP Responses](#mocking-http-responses) +* [Specifying Which Requests are Handled by the Hook](#specifying-which-requests-are-handled-by-the-hook) + * [Filtering by a URL](#filtering-by-a-url) + * [Filtering by a Regular Expression](#filtering-by-a-regular-expression) + * [Filtering by Request Parameters](#filtering-by-request-parameters) + * [Filtering by a Predicate](#filtering-by-a-predicate) +* [Attaching Hooks to Tests and Fixtures](#attaching-hooks-to-tests-and-fixtures) +* [Creating a Custom HTTP Request Hook](#creating-a-custom-http-request-hook) + * [Understanding How TestCafe Request Hooks Operate](#understanding-how-testcafe-request-hooks-operate) + * [Writing a Hook](#writing-a-hook) + +## Logging HTTP Requests + +To log HTTP requests sent during test execution, use the `RequestLogger`. This object stores all requests sent and responses received while the test is running. You can use this information in [assertions](assertions/README.md) to check how the tested page communicates with HTTP services. + +To create a request logger, use the `RequestLogger` constructor. + +```text +RequestLogger([filter] [, options]) +``` + +Parameter | Type | Description | Default +------------ | ---- | ----------- | -------- +`filter` *(optional)* | String | RegExp | Object | Predicate | Specifies which requests should be tracked by the logger. See [Specifying Which Requests are Handled by the Hook](#specifying-which-requests-are-handled-by-the-hook). | All requests are tracked +`options` *(optional)* | Object | Options that define how the requests and responses are logged. | See below + +The `options` parameter contains the following options. + +Option | Type | Description | Default +------ | ---- | ------------- | --------- +`logRequestHeaders` | Boolean | Specifies whether the request headers should be logged. | `false` +`logRequestBody` | Boolean | Specifies whether the request body should be logged. | `false` +`stringifyRequestBody` | Boolean | Specifies whether the request body should be stored as a string or an object. | `false` +`logResponseHeaders` | Boolean | Specifies whether the response headers should be logged. | `false` +`logResponseBody` | Boolean | Specifies whether the response body should be logged. | `false` +`stringifyResponseBody` | Boolean | Specifies whether the response body should be stored as a string or an object. | `false` + +```js +import { RequestLogger } from 'testcafe'; + +const simpleLogger = RequestLogger('http://example.com'); +const headerLogger = RequestLogger(/testcafe/, { + logRequestHeaders: true, + logResponseHeaders: true +}); +``` + +To enable the logger to track the requests, [attach it to a test or fixture](#attaching-hooks-to-tests-and-fixtures). + +By default, `RequestLogger` stores the following parameters. + +* The URL where the request is sent. +* The request's HTTP method. +* The status code received in the response. +* The user agent that sent the request. +* The ID of the test run that sent the request. + +To access data stored by the logger, use its API. + +### Methods + +Method | Return Type | Description +------ | ----------- | ------------- +`contains(predicate)` | Promise | Returns whether the logger contains a request that matches the predicate. +`count(predicate)` | Promise | Returns the number of requests that match the predicate. +`clear()` | None | Clears all logged requests. + +The `predicate` functions take a single parameter - the `Request` object. + +### Properties + +Property | Type | Description +-------- | ---- | ----------- +`requests` | Array of Request | Returns an array of logged requests. + +### The Request Object + +This object represents a request-response pair. + +Property | Type | Description +-------- | ---- | ----------- +`id` | String | The ID of the request (generated by the logger). +`testRunId` | String | The ID of the test run that sent the request. Use it to identify the test in which the request originated. +`userAgent` | String | The user agent that sent the request. +`request.url` | String | The URL where the request is sent. +`request.method` | String | The request's HTTP method. +`request.headers` | Object | Request headers in the property-value form. Logged if the `logRequestHeaders` option is set to `true`. +`request.body` | Object | String | The request body. An object or string depending on the `stringifyRequestBody` option. Logged if the `logRequestBody` option is set to `true`. +`response.statusCode` | Number | The status code received in the response. +`response.headers` | Object | Response headers in the property-value form. Logged if the `logResponseHeaders` option is set to `true`. +`response.body` | Object | String | The response body. An object or string depending on the `stringifyResponseBody` option. Logged if the `logResponseBody` option is set to `true`. + +```js +import { RequestLogger } from 'testcafe'; + +const logger = RequestLogger('http://api.example.com'); + +fixture `test` + .page('http://example.com'); + +test + .requestHooks(logger) + ('test', async t => { + const r = logger.requests[0]; + + console.log(r.id); // UnqLnm189 + console.log(r.testRunId); // IwQA12J12 + console.log(r.userAgent); // Chrome 63.0.3239 / Windows 8.1.0.0 + console.log(r.request.url); // http://api.example.com + console.log(r.request.method); // get + console.log(r.response.statusCode); // 304 + }); +``` + +**Example** + +```js +import { RequestLogger } from 'testcafe'; + +const logger = RequestLogger('http://api.example.com'); + +fixture `test` + .page('http://example.com'); + +test + .requestHooks(logger) + ('test', async t => { + await t.expect(logger.contains(r => r.response.statusCode === 200)).ok(); + }); +``` + +## Mocking HTTP Responses + +Mocking is useful when the tested app uses a piece of infrastructure that is difficult to deploy during the test run. In this instance, you can intercept requests to this resource and mock the responses using TestCafe. + +To create a response mocker, use the `RequestMock` constructor. + +```js +var mock = RequestMock() +``` + +Then call the `onRequestTo` and `respond` methods in a chain. The `onRequestTo` method specifies a request to intercept, while the `respond` method specifies the mocked response for this request. Repeat calling these methods to provide a mock for every request you need. + +```js +var mock = RequestMock() + .onRequestTo(request1) + .respond(responseMock1) + .onRequestTo(request2) + .respond(responseMock2); +``` + +To enable the hook to mock the requests, [attach it to a test or fixture](#attaching-hooks-to-tests-and-fixtures). + +### onRequestTo + +```text +onRequestTo([filter]) +``` + +Parameters | Type | Description | Default +---------- | ---- | ----------- | ----- +`filter` *(optional)* | String | RegExp | Object | Predicate | Specifies which requests should be mocked with a response that follows in the `respond` method. See [Specifying Which Requests are Handled by the Hook](#specifying-which-requests-are-handled-by-the-hook). | All requests are mocked. + +```js +var mock = RequestMock() + .onRequestTo('http://external-service.com/api/') + .respond(/*...*/) + .onRequestTo(/\/users\//) + .respond(/*...*/); +``` + +### respond + +```text +respond(body [, statusCode] [, headers]) +``` + +Parameter | Type | Description +--------- | ---- | ------------- +`body` | Object | String | Function | A mocked response body. Pass an object for a JSON response, a string for an HTML response or a function to build a custom response. +`statusCode` *(optional)* | Number | The response status code. +`headers` *(optional)* | Object | Custom headers added to the response in the property-value form. + +```js +var mock = RequestMock() + .onRequestTo(/*...*/) + .respond({ data: 123 }) // a JSON response + .onRequestTo(/*...*/) + .respond('') // an HTML response + .onRequestTo(/*...*/) + .respond(null, 204) // an empty response with a status code + .onRequestTo(/*...*/) + .respond('', 200, { // a response with custom headers + 'server': 'nginx/1.10.3' + }) + .onRequestTo(/*...*/) + .respond(function (req, res) { // a custom response + res.headers['x-calculated-header'] = 'calculated-value'; + res.statusCode = '200'; + + const parsedUrl = url.parse(req.path, true); + + res.setBody('calculated body' + parsedUrl.query['param']); + }); +``` + +A custom response function takes two parameters. + +Parameter | Type | Description +--------- | ---- | --------------- +`req` | Object | A request to be mocked. +`res` | Object | A mocked response. + +Use information about the request provided in the `req` parameter to configure the response via the `res` parameter. + +The `req` parameter exposes the following members. + +Property | Type | Description +-------- | ---- | ------------ +`headers` | Object | The request headers in the property-value form. +`body` | Object | The request body. +`url` | String | A URL to which the request is sent. +`protocol` | String | The request protocol. +`hostname` | String | The destination host name. +`host` | String | The destination host. +`port` | Number | The destination port. +`path` | String | The destination path. +`method` | String | The request method. +`credentials` | Object | Credentials that were used to authenticate in the current session using NTLM or Basic authentication. For HTTP Basic authentication, these are `username` and `password`. NTLM authentication additionally specifies `workstation` and `domain`. +`proxy` | Object | If a proxy is used, contains information about its `host`, `hostname`, `port`, `proxyAuth`, `authHeader` and `bypassRules`. + +Use the following members exposed by `res` to configure the response. + +Property | Type | Description +-------- | ---- | ------------ +`headers` | Object | The response headers. +`statusCode` | Number | The response status code. + +Method | Description +------ | --------------- +`setBody(value)` | Sets the `value` as the response body. + +## Specifying Which Requests are Handled by the Hook + +The request logger, mock and custom request hooks require that you specify which requests should be handled by them and which should be skipped. + +You can perform this filtering by passing the *request filtering rules* to the hook. Note that you can pass a single rule or an array of those. + +### Filtering by a URL + +Pass a string with a URL to intercept all requests sent to this URL. + +```js +const logger = RequestLogger('http://example.com'); +``` + +```js +const mock = RequestMock() + .onRequestTo('http://external-service.com/api/') + .respond(/*...*/); +``` + +### Filtering by a Regular Expression + +You can also specify a regular expression that matches the desired URLs. + +```js +const logger = RequestLogger(/.co.uk/); +``` + +```js +const mock = RequestMock() + .onRequestTo('/\/api\/users\//') + .respond(/*...*/); +``` + +### Filtering by Request Parameters + +You can filter requests by a combination of the URL and the request method. + +In this instance, you need to use an object that contains the following fields. + +Property | Type | Description +-------- | ---- | ------------ +`url` | String | A URL to which a request is sent. +`method` | String | The request method. +`isAjax` | Boolean | Specifies whether this is an AJAX request. + +```js +const logger = RequestLogger({ url: 'http://example.com', method: 'GET', isAjax: false }); +``` + +```js +const mock = RequestMock() + .onRequestTo({ url: 'http://external-service.com/api/', method: 'POST', isAjax: true }) + .respond(/*...*/); +``` + +### Filtering by a Predicate + +In case you need more request parameters to decide whether to handle the request or not, you can use a predicate function. + +```js +const logger = RequestLogger(function (request) { + return request.url === 'http://example.com' && + request.method === 'post' && + request.isAjax && + request.body === '{ test: true }' && + request.headers['content-type'] === 'application/json'; +}); +``` + +This predicate takes the `request` parameter that provides the following properties. + +Property | Type | Description +-------- | ---- | -------------- +`requestId` | String | TestCafe internal request ID. +`userAgent` | String | The user agent that originated the request. +`url` | String | The URL to which the request is sent. +`method` | String | The request method. +`isAjax` | Boolean | Specifies whether this is an AJAX request. +`headers` | Object | The request headers in the property-value form. +`body` | Object | The request body. +`testRunId` | String | TestCafe internal ID of the test run that sent the request. + +## Attaching Hooks to Tests and Fixtures + +To attach a hook to a test or fixture, use the `fixture.requestHooks` and `test.requestHooks` methods. A hook attached to a fixture will handle requests from all tests in the fixture. + +```text +fixture.requestHooks(...hook) +test.requestHooks(...hook) +``` + +Parameter | Type | Description +--------- | ---- | ------------ +`hook` | RequestHook | A request logger, mock or custom hook. + +The `requestHooks` methods use the rest operator, which allows you to pass multiple hooks as parameters or arrays of hooks. + +```js +import { RequestLogger, RequestMock } from 'testcafe'; + +const logger = RequestLogger('http://example.com'); +const mock = RequestMock() + .onRequestTo('http://external-service.com/api/') + .respond({ data: 'value' }); + +fixture `My fixture` + .page('http://example.com') + .requestHooks(logger); + +test + .requestHooks(mock) + ('My test', async t => { + // test actions + }) +``` + +You can also attach and detach hooks during test run. To do this, use the `t.addRequestHooks` and `t.removeRequestHooks` methods. + +```text +t.addRequestHooks(...hook) +t.removeRequestHooks(...hooks) +``` + +Parameter | Type | Description +--------- | ---- | ------------ +`hook` | RequestHook | A request logger, mock or custom hook. + +The `t.addRequestHooks` and `t.removeRequestHooks` methods use the rest operator, which allows you to pass multiple hooks as parameters or arrays of hooks. + +```js +import { RequestLogger } from 'testcafe'; + +const logger = RequestLogger('http://example.com'); + +fixture `My fixture` + .page('http://example.com'); + +test('My test', async t => { + await t + .click('#send-unlogged-request') + .addRequestHooks(logger) + .click('#send-logged-request') + .expect(logger.count(() => true)).eql(1) + .removeRequestHooks(logger) + .click('#send-unlogged-request'); +}) +``` + +## Creating a Custom HTTP Request Hook + +To handle HTTP requests in a custom way, you can create your own request hook. But first, you need to know how TestCafe request hooks work. + +### Understanding How TestCafe Request Hooks Operate + +* All TestCafe request hooks descend from the `RequestHook` class. + + ```js + class MyRequestHook extends RequestHook { + // ... + } + ``` + +* To determine which requests the hook handles, the base class constructor receives an array of [filtering rules](#specifying-which-requests-are-handled-by-the-hook) as the first parameter. If no rules are passed, all requests are handled. + + ```js + export default class RequestHook { + constructor (requestFilterRules, /* other params */) { + console.log(requestFilterRules[0]); // http://example.com + console.log(requestFilterRules[1]); // /\/api\/users\// + } + ``` + +* Before sending the request, the `onRequest` method is called. Use this method to handle the request sending. If necessary, you can change the request parameters before it is sent. + + This method is abstract in the base class and needs to be overriden in the descendant. + + ```js + onRequest (/*RequestEvent event*/) { + throw new Error('Not implemented'); + } + ``` + +* When a response is received, first an internal `_onConfigureResponse` event fires. This event configures the call to the `onResponse` method that follows. + + The `_onConfigureResponse` method processes settings that specify whether to pass the response headers and body to the `onResponse` method. These settings are specified in the second constructor parameter. This parameter takes an object with two properties - `includeHeaders` and `includeBody`. + + ```js + export default class RequestHook { + constructor (requestFilterRules, responseEventConfigureOpts) { + console.log(responseEventConfigureOpts.includeHeaders); // false + console.log(responseEventConfigureOpts.includeBody); // false + } + ``` + +* After `_onConfigureResponse` is handled, the `onResponse` method is called. This method is abstract in the base class. Override it in the descendant to handle the request sending. + + ```js + onResponse (/*ResponseEvent event*/) { + throw new Error('Not implemented'); + } + ``` + +### Writing a Hook + +To sum up, this is what you need to write a custom hook. + +* inherit from the `RequestHook` class, +* override the `onRequest` method to handle sending the request, +* override the `onResponse` method to handle receiving the response. + +```js +import { RequestHook } from 'testcafe'; + +class MyRequestHook extends RequestHook { + constructor (requestFilterRules, responseEventConfigureOpts) { + super(requestFilterRules, responseEventConfigureOpts); + // ... + } + onRequest (event) { + // ... + } + onResponse (event) { + // ... + } +} +``` + +The `onRequest` and `onResponse` methods receive an object that contains the event parameters. + +The `onRequest` method's `event` object exposes the following fields. + +Property | Type | Description +-------- | ---- | -------------- +`requestOptions` | Object | Contains the request parameters. You can use it to change the request parameters before the request is sent. +`isAjax` | Boolean | Specifies if the request is performed using AJAX. + +The `requestOptions` object has the following properties. + +Property | Type | Description +-------- | ---- | ------------ +`headers` | Object | The request headers in the property-value form. +`body` | Object | The request body. +`url` | String | A URL to which the request is sent. +`protocol` | String | The request protocol. +`hostname` | String | The destination host name. +`host` | String | The destination host. +`port` | Number | The destination port. +`path` | String | The destination path. +`method` | String | The request method. +`credentials` | Object | Credentials that were used to authenticate in the current session using NTLM or Basic authentication. For HTTP Basic authentication, these are `username` and `password`. NTLM authentication additionally specifies `workstation` and `domain`. +`proxy` | Object | If a proxy is used, contains information about its `host`, `hostname`, `port`, `proxyAuth`, `authHeader` and `bypassRules`. + +```js +onRequest (event) { + if(event.isAjax) { + console.log(event.requestOptions.url); + console.log(event.requestOptions.credentials.username); + + event.requestOptions.headers['custom-header'] = 'value'; + } +} +``` + +The `onResponse` method's `event` object exposes the following properties. + +Property | Type | Description +-------- | ---- | -------------- +`statusCode` | Number | The response status code. +`headers` | Object | The response headers in a property-value form. +`body` | Object | The response body. + +```js +onResponse (event) { + if(event.statusCode === 200) + console.log(event.headers['Content-Type']); +} +``` + +Now you can [attach this hook to a test or fixture](#attaching-hooks-to-tests-and-fixtures) in your test suite and start using it. + +```js +import { MyRequestHook } from './my-request-hook'; + +const customHook = new MyRequestHook('http://example.com'); + +fixture `My fixture` + .page('http://example.com') + .requestHooks(customHook); + +test('My test', async t => { + // test actions +}); +``` \ No newline at end of file diff --git a/docs/nav/nav-menu.yml b/docs/nav/nav-menu.yml index 7b25b319e19..0eb5dfa5fc0 100644 --- a/docs/nav/nav-menu.yml +++ b/docs/nav/nav-menu.yml @@ -128,6 +128,8 @@ content: - name: Examples of Using Client Functions url: /documentation/test-api/obtaining-data-from-the-client/examples-of-using-client-functions.md + - name: Intercepting and Mocking HTTP Requests + url: /documentation/test-api/intercepting-and-mocking-http-requests.md - name: Waiting for Page Elements to Appear url: /documentation/test-api/waiting-for-page-elements-to-appear.md - name: Authentication