Skip to main content

externalCallAnApi

An ability that wraps axios client and enables the actor to send HTTP requests to HTTP APIs.

CallAnApi uses proxy-from-env and an approach described in "Node.js Axios behind corporate proxies" to automatically detect proxy server configuration based on your environment variables. You can override this configuration if needed.

Configuring the ability to call an API

The easiest way to configure the ability to CallAnApi is to provide the baseURL of your HTTP API, and rely on Serenity/JS to offer other sensible defaults:

import { actorCalled } from '@serenity-js/core'
import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

await actorCalled('Apisitt')
.whoCan(
CallAnApi.at('https://api.example.org/')
)
.attemptsTo(
Send.a(GetRequest.to('/v1/users/2')), // GET https://api.example.org/v1/users/2
Ensure.that(LastResponse.status(), equals(200)),
)

Resolving relative URLs

Serenity/JS resolves request URLs using Node.js WHATWG URL API. This means that the request URL is determined using the resource path resolved in the context of base URL, i.e. new URL(resourcePath, [baseURL]).

Consider the following example:

import { actorCalled } from '@serenity-js/core'
import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

await actorCalled('Apisitt')
.whoCan(
CallAnApi.at(baseURL)
)
.attemptsTo(
Send.a(GetRequest.to(resourcePath)),
Ensure.that(LastResponse.status(), equals(200)),
)

In the above example:

  • when resourcePath is defined as a full URL, it overrides the base URL
  • when resourcePath starts with a forward slash /, it replaces any path defined in the base URL
  • when resourcePath is not a full URL and doesn't start with a forward slash, it gets appended to the base URL
baseURLresourcePathresult
https://api.example.org//v1/users/2https://api.example.org/v1/users/2
https://example.org/api/v1/users/2https://example.org/api/v1/users/2
https://example.org/api/v1//secure/oauthhttps://example.org/secure/oauth
https://v1.example.org/api/https://v2.example.org/https://v2.example.org/

Using Axios configuration object

When you need more control over how your Axios instance is configured, provide an Axios configuration object. For example:

import { actorCalled } from '@serenity-js/core'
import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

await actorCalled('Apisitt')
.whoCan(
CallAnApi.using({
baseURL: 'https://api.example.org/',
timeout: 30_000,
// ... other configuration options
})
)
.attemptsTo(
Send.a(GetRequest.to('/users/2')),
Ensure.that(LastResponse.status(), equals(200)),
)

Working with proxy servers

CallAnApi uses proxy-from-env to automatically detect proxy server configuration based on your environment variables.

This default behaviour can be overridden by providing explicit proxy configuration:

import { actorCalled } from '@serenity-js/core'
import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

await actorCalled('Apisitt')
.whoCan(
CallAnApi.using({
baseURL: 'https://api.example.org/',
proxy: {
protocol: 'http',
host: 'proxy.example.org',
port: 9000,
auth: { // `auth` is optional
username: 'proxy-username',
password: 'proxy-password',
}
}
// ... other configuration options
})
)
.attemptsTo(
Send.a(GetRequest.to('/users/2')),
Ensure.that(LastResponse.status(), equals(200)),
)

Please note that Serenity/JS uses proxy-agents and the approach described in "Node.js Axios behind corporate proxies" to work around limited proxy support capabilities in Axios itself.

Using Axios instance with proxy support

To have full control over the Axios instance used by the ability to CallAnApi, you can create it yourself and inject it into the ability. This approach allows you to still benefit from automated proxy detection in configuration, while taking advantage of the many Axios plugins.

import { actorCalled } from '@serenity-js/core'
import { createAxios, CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

import axiosRetry from 'axios-retry'

const instance = createAxios({ baseURL 'https://api.example.org/' })
axiosRetry(instance, { retries: 3 })

await actorCalled('Apisitt')
.whoCan(
CallAnApi.using(instance)
)
.attemptsTo(
Send.a(GetRequest.to('/users/2')),
Ensure.that(LastResponse.status(), equals(200)),
)

Using raw Axios instance

If you don't want Serenity/JS to enhance your Axios instance with proxy support, instantiate the ability to CallAnApi using its constructor directly. Note, however, that by using this approach you're taking the responsibility for all the aspects of configuring Axios.

import { actorCalled } from '@serenity-js/core'
import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

import { axiosCreate } from '@serenity-js/rest'
import axiosRetry from 'axios-retry'

const instance = axiosCreate({ baseURL 'https://api.example.org/' })
axiosRetry(instance, { retries: 3 })

await actorCalled('Apisitt')
.whoCan(
new CallAnApi(instance) // using the constructor ensures your axios instance is not modified in any way.
)
.attemptsTo(
// ...
)

Serenity/JS defaults

When using CallAnApi.at or CallAnApi.using with a configuration object, Serenity/JS merges your Axios request configuration with the following defaults:

  • timeout: 10 seconds

You can override them by specifying the given property in your configuration object, for example:

import { actorCalled } from '@serenity-js/core'
import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

await actorCalled('Apisitt')
.whoCan(
CallAnApi.using({
baseURL: 'https://api.example.org/',
timeout: 30_000
})
)
.attemptsTo(
Send.a(GetRequest.to('/users/2')),
Ensure.that(LastResponse.status(), equals(200)),
)

Interacting with multiple APIs

Some test scenarios might require you to interact with multiple HTTP APIs. With Serenity/JS you can do this using either API-specific actors, or by specifying full URLs when performing the requests.

The following examples will assume that the test scenarios needs to interact with the following APIs:

  • https://testdata.example.org/api/v1/
  • https://shop.example.org/api/v1/

Let's also assume that the testdata API allows the automation to manage the test data used by the shop API.

Using API-specific actors

To create API-specific actors, configure your test runner with a cast that gives your actors appropriate abilities based, for example, on their name:

import { beforeEach } from 'mocha'
import { Actor, Cast, engage } from '@serenity-js/core'
import { CallAnApi } from '@serenity-js/rest'

export class MyActors implements Cast {
prepare(actor: Actor): Actor {
switch(actor.name) {
case 'Ted':
return actor.whoCan(CallAnApi.at('https://testdata.example.org/api/v1/'))
case 'Shelly':
return actor.whoCan(CallAnApi.at('https://shop.example.org/api/v1/'))
default:
return actor;
}
}
}

beforeEach(() => engage(new MyActors()))

Next, retrieve the appropriate actor in your test scenario using actorCalled, for example:

import { describe, it, beforeEach } from 'mocha'
import { actorCalled, engage } from '@serenity-js/core
import { Send, GetRequest, PostRequest, LastResponse } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

describe('Multi-actor API testing', () => {
beforeEach(() => engage(new MyActors()))

it('allows each actor to interact with their API', async () => {

await actorCalled('Ted').attemptsTo(
Send.a(PostRequest.to('products').with({ name: 'Apples', price: '£2.50' })),
Ensure.that(LastResponse.status(), equals(201)),
)

await actorCalled('Shelly').attemptsTo(
Send.a(GetRequest.to('?product=Apples')),
Ensure.that(LastResponse.status(), equals(200)),
Ensure.that(LastResponse.body(), equals([
{ name: 'Apples', price: '£2.50' }
])),
)
})
})

Using full URLs

If you prefer to have a single actor interacting with multiple APIs, you can specify the full URL for every request:

import { describe, it, beforeEach } from 'mocha'
import { actorCalled, Cast, engage } from '@serenity-js/core
import { CallAnApi, Send, GetRequest, PostRequest, LastResponse } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

describe('Multi-actor API testing', () => {
beforeEach(() => engage(
Cast.where(actor => actor.whoCan(CallAnApi.using({})))
))

it('allows each actor to interact with their API', async () => {

await actorCalled('Alice').attemptsTo(
Send.a(PostRequest.to('https://testdata.example.org/api/v1/products')
.with({ name: 'Apples', price: '£2.50' })),
Ensure.that(LastResponse.status(), equals(201)),

Send.a(GetRequest.to('https://shop.example.org/api/v1/?product=Apples')),
Ensure.that(LastResponse.status(), equals(200)),
Ensure.that(LastResponse.body(), equals([
{ name: 'Apples', price: '£2.50' }
])),
)
})
})

Learn more

Hierarchy

Index

Constructors

externalconstructor

  • new CallAnApi(axiosInstance: AxiosInstance): CallAnApi
  • Learn more


    Parameters

    • externalaxiosInstance: AxiosInstance

      A pre-configured instance of the Axios HTTP client

    Returns CallAnApi

Methods

staticexternalat

  • Produces an ability to call a REST API at a specified baseURL;

    This is the same as invoking CallAnApi.using({ baseURL: 'https://example.org' })


    Parameters

    • externalbaseURL: string | URL

    Returns CallAnApi

staticexternalusing

  • Produces an ability to call an HTTP API using the given Axios instance, or an Axios request configuration object.

    When you provide an Axios configuration object, it gets shallow-merged with the following defaults:

    When you provide an Axios instance, it's enhanced with proxy support and no other modifications are made.

    If you don't want Serenity/JS to augment or modify your Axios instance in any way, please use the CallAnApi.constructor directly.


    Parameters

    Returns CallAnApi

externalabilityType

  • Returns the most abstract type of this Ability instance, specifically the first class in the inheritance hierarchy that directly extends the Ability class.

    import { Ability } from '@serenity-js/core';

    class MyAbility extends Ability {}
    class MySpecialisedAbility extends MyAbility {}

    new MyAbility().abilityType(); // returns MyAbility
    new MySpecialisedAbility().abilityType(); // returns MyAbility

    Returns AbilityType<Ability>

externalmodifyConfig

  • modifyConfig(fn: (original: AxiosDefaults<any>) => any): void
  • Allows for the original Axios config to be changed after the ability to CallAnApi has been instantiated and given to the Actor.

    Learn more


    Parameters

    • externalfn: (original: AxiosDefaults<any>) => any

      Returns void

    externalrequest

    • request(config: AxiosRequestConfig<any>): Promise<AxiosResponse<any, any>>
    • Sends an HTTP request to a specified url. Response will be cached and available via CallAnApi.mapLastResponse.

      Learn more


      Parameters

      • externalconfig: AxiosRequestConfig<any>

        Axios request configuration, which can be used to override the defaults provided when the ability to CallAnApi was instantiated.

      Returns Promise<AxiosResponse<any, any>>

    externalresolveUrl

    • resolveUrl(config: AxiosRequestConfig<any>): string
    • Resolves the final URL, based on the AxiosRequestConfig provided and any defaults that the AxiosInstance has been configured with.

      Note that unlike Axios, this method uses the Node.js WHATWG URL API to ensure URLs are correctly resolved.


      Parameters

      • externalconfig: AxiosRequestConfig<any>

      Returns string

    externalmapLastResponse

    • mapLastResponse<T>(mappingFunction: (response: AxiosResponse<any, any>) => T): T
    • Maps the last cached response to another type. Useful when you need to extract a portion of the AxiosResponse object.

      Learn more


      Type parameters

      • T

      Parameters

      • externalmappingFunction: (response: AxiosResponse<any, any>) => T

        Returns T

      externaltoJSON