Testcafe: Implement request hooks

Created on 22 Mar 2017  Â·  18Comments  Â·  Source: DevExpress/testcafe

Are you requesting a feature or reporting a bug?

feature

What is the current behavior?

You can't intercept HTTP requests

What is the expected behavior?

Add API to intercept HTTP requests:

  • export RequestHook abstract class from which implementations could inherit;
  • add t.addRequestHook, test.useRequestHook and fixture.useRequestHook methods.
  • add t.removeRequestHook methods

Having this mechanism we can implement RequestLogger (#1270) and RequestMock (#1271)

server Auto-locked API hammerhead enhancement

Most helpful comment

Request Hook

API for adding/removing

  • fixture.requestHooks(...)
  • test.requestHooks(...)
  • t.addRequestHooks(...), t.removeRequestHooks(...)

where ... is rest parameter. Also passed array will be flatten to plain list.

Examples of usage

fixture.requestHooks(hook1);
test.requestHooks(hook1, hook2, ...);
fixture.requestHooks([hook1, hook2], hook3);
test.requestHooks([hook1, hook2], hook3, [hook4, hook5]);

Exporting classes

  • RequestLogger - allows to collect http requests by specified rules (see RequestFilterRule section).
  • RequestMock - allows to return test responses for specified http requests
  • RequestHook - the base class, allows to implement a custom logic to intercept http requests.

RequestLogger

RequestLogger(filter, logOptions)

filter - rule for filtering http requests (see RequestFilterRule description below)

logOptions - determinate which parts of request should collect.

Default values for logOptions:

{
    logRequestHeaders: false,
    logRequestBody: false,
    stringifyRequestBody: false,
    logResponseHeaders: false,
    logResponseBody: false,
    stringifyResponseBody: false
}

i.e. by default the RequestLogger collects only url, statusCode, userAgent, sessionId(internal) request properties.

API

Methods

|Name | Returned type | Description |
|-------------------------- | ------------------- | ------------------------------------------------------|
|async contains(predicate)| Promise | Finds requests by predicate and returns true/false depending on result |
|async count(predicate) |Promise | Finds requests by predicate and returns their count. |
|clear () | None | Clears collected requests |

Properties

| Name | Type | Description
|----------|-------|----------
|requests| Array | returns collected requests as Array

For async function the build-in Smart Assertion Query mechanism will be applied.

Example

import { RequestLogger } from 'testcafe';

const logger = RequestLogger('https://example.com');

fixture `test`
    .page('https://example.com');

test
    .requestHooks(logger)
    ('test', async t => {
        await t.expect(logger.contains(r => r.response.statusCode === 200)).ok();
    });

Request Mock

API

We build our API depending on nock library.

RequestMock configures with pairs of methods .onRequestTo(requestFilterRule).respond(responseMock).

Example

var mock = RequestMock()
            .onRequestTo(requestFilterRule1)
            .respond(responseMock1)
            .onRequestTo(requestFilterRule2)
            .respond(responseMock2)

where information about RequestFilterRule see below, ResponseMock - allows to construct response using various arguments.

Example

import { RequestMock } from 'testcafe';

const requestMock = RequestMock()
    .onRequestTo('http://external-service.com/api/users'}) /*see RequestFilterRule*/
    .respond({data: 123}) /* JSON response */
    .onRequestTo(...)
    .respond('The error is occured!!!') /*HTML response*/
    .onRequestTo (...)
    .respond(null, 204) /*custom statusCode*/
    .onRequestTo(...)
    .respond('<html_markup>', 200, { 'server': 'nginx/1.10.3' }) /* custom headers */
    .onRequestTo(...)
    .respond(function (req, res){ /* respond function */          
          res.headers[‘x-calculated-header’] = ‘calculated-value’;
          res.statusCode = ‘200’;

          const responseBody = fs.readFileSync(req.params['filename']).toString();
          res.setBody(responseBody);
    });


fixture `Fixture`
    .page(‘http://example.com/’)
    .rquestHooks(requestMock);

test('test', () => {
    await t.click(‘body’);

})
````

## RequestFilterRule
Allows to specify request filtering rule for requests.
It's a public term that implemented with an internal class. 
In public API we will use a simple object construction syntax.

**Examples**

```js
/*String*/
'http://example.com' -> RequestFilterRule(`'http://example.com'`) 

/*Regular expession*/
/example.com/ -> RequestFilterRule(`/example.com/`) 

/*Object with properties*/
{ url: 'http://example.com', method: 'GET', isAjax: false } -> RequestFilterRule(`{ url: 'http://example.com', method: 'GET', isAjax: false }`) 

 /*Custom function*/
RequestFilterRule(function (request) {
            return request.url === 'http://example.com' &&
                   request.method === 'post' &&
                   request.isAjax &&
                   request.body === '{ test: true }' &&
                   request.headers['content-type'] === 'application/json';
}

All 18 comments

Just want to add my use case for this feature.

I'm currently building a SPA that has a .net web api backend.
TestCafe seems to be awesome for functional UI testing, but we have another type of testing that we perform: API testing in a regression environment.

At the moment we're trialling using SoapUI for this kind of testing but are finding it a little clunky.
Our entire application requires a user to be authenticated.
Testing the API which requires authentication can be achieved in SoapUI, but we have to jump through some hoops by:

  1. Actually requesting the login page as we use request verification tokens
  2. Posting the form back
  3. Capturing the auth cookie
  4. Attach the auth cookie to each subsequent API call

All of this is trivial to achieve in TestCafe as it's actually running in the browser.

In our regression environment we then call our API and compare the results to a saved json file. As it's a financial application, some of the saved results can be quite large and property level testing is not appropriate - we usually just want to ensure that the entire response is the same as the previous test run.

I'd like to use TestCafe to login, navigate to a report screen, fill out the report parameters, run the report and then intercept the xhr request for the results - this might be an xhr request that I'd like to wait several minutes for.
I'd then like to do jest-style snapshot testing of the json results i.e.

t.expect(jsonResults).toMatchSnapshot();

I'd be happy taking a dependency on another library (think Ava rely on Jest to do a similar assertion) to do that snapshot.

If I could do this in TestCafe, then I'd have one coherent strategy and framework for all functional UI and API testing which would be 💯

Is there any movement on this issue? Looks like it's planned for development. Any timeframe on this?

Hi @oneillci,

Thanks for your interest.
We've done some preparations for this feature in our proxy and we plan implement this in the next release iteration (after the current one). It can take about a couple of months.

Would love to use this feature too!

Request Hook

API for adding/removing

  • fixture.requestHooks(...)
  • test.requestHooks(...)
  • t.addRequestHooks(...), t.removeRequestHooks(...)

where ... is rest parameter. Also passed array will be flatten to plain list.

Examples of usage

fixture.requestHooks(hook1);
test.requestHooks(hook1, hook2, ...);
fixture.requestHooks([hook1, hook2], hook3);
test.requestHooks([hook1, hook2], hook3, [hook4, hook5]);

Exporting classes

  • RequestLogger - allows to collect http requests by specified rules (see RequestFilterRule section).
  • RequestMock - allows to return test responses for specified http requests
  • RequestHook - the base class, allows to implement a custom logic to intercept http requests.

RequestLogger

RequestLogger(filter, logOptions)

filter - rule for filtering http requests (see RequestFilterRule description below)

logOptions - determinate which parts of request should collect.

Default values for logOptions:

{
    logRequestHeaders: false,
    logRequestBody: false,
    stringifyRequestBody: false,
    logResponseHeaders: false,
    logResponseBody: false,
    stringifyResponseBody: false
}

i.e. by default the RequestLogger collects only url, statusCode, userAgent, sessionId(internal) request properties.

API

Methods

|Name | Returned type | Description |
|-------------------------- | ------------------- | ------------------------------------------------------|
|async contains(predicate)| Promise | Finds requests by predicate and returns true/false depending on result |
|async count(predicate) |Promise | Finds requests by predicate and returns their count. |
|clear () | None | Clears collected requests |

Properties

| Name | Type | Description
|----------|-------|----------
|requests| Array | returns collected requests as Array

For async function the build-in Smart Assertion Query mechanism will be applied.

Example

import { RequestLogger } from 'testcafe';

const logger = RequestLogger('https://example.com');

fixture `test`
    .page('https://example.com');

test
    .requestHooks(logger)
    ('test', async t => {
        await t.expect(logger.contains(r => r.response.statusCode === 200)).ok();
    });

Request Mock

API

We build our API depending on nock library.

RequestMock configures with pairs of methods .onRequestTo(requestFilterRule).respond(responseMock).

Example

var mock = RequestMock()
            .onRequestTo(requestFilterRule1)
            .respond(responseMock1)
            .onRequestTo(requestFilterRule2)
            .respond(responseMock2)

where information about RequestFilterRule see below, ResponseMock - allows to construct response using various arguments.

Example

import { RequestMock } from 'testcafe';

const requestMock = RequestMock()
    .onRequestTo('http://external-service.com/api/users'}) /*see RequestFilterRule*/
    .respond({data: 123}) /* JSON response */
    .onRequestTo(...)
    .respond('The error is occured!!!') /*HTML response*/
    .onRequestTo (...)
    .respond(null, 204) /*custom statusCode*/
    .onRequestTo(...)
    .respond('<html_markup>', 200, { 'server': 'nginx/1.10.3' }) /* custom headers */
    .onRequestTo(...)
    .respond(function (req, res){ /* respond function */          
          res.headers[‘x-calculated-header’] = ‘calculated-value’;
          res.statusCode = ‘200’;

          const responseBody = fs.readFileSync(req.params['filename']).toString();
          res.setBody(responseBody);
    });


fixture `Fixture`
    .page(‘http://example.com/’)
    .rquestHooks(requestMock);

test('test', () => {
    await t.click(‘body’);

})
````

## RequestFilterRule
Allows to specify request filtering rule for requests.
It's a public term that implemented with an internal class. 
In public API we will use a simple object construction syntax.

**Examples**

```js
/*String*/
'http://example.com' -> RequestFilterRule(`'http://example.com'`) 

/*Regular expession*/
/example.com/ -> RequestFilterRule(`/example.com/`) 

/*Object with properties*/
{ url: 'http://example.com', method: 'GET', isAjax: false } -> RequestFilterRule(`{ url: 'http://example.com', method: 'GET', isAjax: false }`) 

 /*Custom function*/
RequestFilterRule(function (request) {
            return request.url === 'http://example.com' &&
                   request.method === 'post' &&
                   request.isAjax &&
                   request.body === '{ test: true }' &&
                   request.headers['content-type'] === 'application/json';
}

@miherlosev is this functionality available somewhere now, or is this just the planned interface?

@curtisblackwell This is in progress now. @miherlosev already have created a pull request but it requires some time to finish it. If you are interested in this we'll be able to provide you with a dev build once this feature is merged

This looks really promising. I have a question though. Will you be able to assert that a request-hook has been triggered?

Will you be able to assert that a request-hook has been triggered?

I am not sure understand your question.
For which case does it need?

@AlexanderMoskovkin understood, thank you. I'm interested, but it sounds like timing may be an issue.

Thinking about it, it's probably not actually needed for integration tests.

Will you release a dev version so that we can try out the request hooks feature?

@elgreco247
Thank you for your interest in this feature.
 
The Request Hooks feature significantly affects the TestCafe subsystems. First, we will perform internal testing. After that a public development version will be released.

I notice that at least some of this feature is present in the alpha 2 build. Could you explain to me how to use it to achieve what I want to achieve.
https://testcafe-discuss.devexpress.com/t/testcafe-not-always-catching-requests-from-browser/818/4
that is:-
Our webapp gets a series of images when a report is run.
When I run the report via test cafe each image is GOT twice, once without the proxy wrapper and without our internal auth cookie, and once with the proxy wrapper (the cookie is not visible but it works anyway, so I'm assuming some sort of proxy magic). We need to be able to prevent testcafe from attempting to get these images without the correct authorisation. Because doing so sets off alarms in our data dog as they look like attacks. Without being able to do this we will be forced to throw away a few months of work and switch to a different technology.

Feature Request Hooks is ready for testing. Anyone can try this.

Before testing you need to install the latest alpha version - [email protected].
Final documentation is in progress, but you can already use this PR.

The TypeScript typings will be added in separate PR.

This thread has been automatically locked since it is closed and there has not been any recent activity. Please open a new issue for related bugs or feature requests. We recommend you ask TestCafe API, usage and configuration inquiries on StackOverflow.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

inikulin picture inikulin  Â·  3Comments

AndreyBelym picture AndreyBelym  Â·  3Comments

Turkirafaa picture Turkirafaa  Â·  3Comments

hinok picture hinok  Â·  3Comments

Lukas-Kullmann picture Lukas-Kullmann  Â·  3Comments