Skip to main content

Assertions and expectations

Serenity/JS helps you model your test scenarios from the perspective of actors performing activities to accomplish their goals. Assertions follow this same consistent approach, with any assertions expressed using the interaction to Ensure.

The interaction to Ensure has two flavours:

  • Ensure.that, which makes the actor evaluate the expectation, and fails immediately if its condition is not met,
  • Ensure.eventually, which keeps evaluating the expectation until the condition is met, or the interaction timeout expires.

The anatomy of a Serenity/JS assertion​

Both Ensure.that and Ensure.eventually follow the same consistent pattern and accept two arguments:

An example Serenity/JS assertion might look like this:

import { actorCalled } from '@serenity-js/core'
import { Ensure, and, startsWith, endsWith } from '@serenity-js/assertions'

await actorCalled('Edna').attemptsTo(
Ensure.that('Hello world', and(startsWith('Hello'), endsWith('world'))),
// actual value --^ ^-- expectation
)

Note that several Serenity/JS modules provide expectations you can use to define your assertions, most notably:

Reusable assertions​

Unlike other assertion libraries, Serenity/JS allows for the "actual value" to be determined dynamically and resolved in the context of the actor performing the assertion. This design enables great flexibility and helps to maximise opportunities for code reuse.

Consider a simple test scenario, verifying that an interaction with a REST API returns the status code of 200 OK:

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

await actorCalled('Apisit')
.whoCan(CallAnApi.at('https://serenity-js.org/'))
.attemptsTo(
Send.a(GetRequest.to('/handbook/design/assertions')),
Ensure.that(LastResponse.status(), equals(200)),
)

Since the question about the LastResponse.status() is evaluated dynamically by the actor who performed the GetRequest, you could create a custom task that encapsulates this operation:

import { Answerable, d, Task } from '@serenity-js/rest'
import { Send, GetRequest, LastResponse } from '@serenity-js/rest'
import { Ensure, equals } from '@serenity-js/assertions'

const checkUrl = (url: Answerable<string>) =>
Task.where(d`#actor checks the ${ url }`,
Send.a(GetRequest.to(url)),
Ensure.that(LastResponse.status(), equals(200)),
)

You could then use such custom task to create a simple link checker:

import { actorCalled, List } from '@serenity-js/core'
import { CallAnApi } from '@serenity-js/rest'

await actorCalled('Apisit')
.whoCan(CallAnApi.at('https://serenity-js.org/'))
.attemptsTo(
checkUrl('/handbook/design/assertions'),
)

Or even combine it with a List to check multiple URLs one after another:

import { actorCalled, List } from '@serenity-js/core'
import { CallAnApi } from '@serenity-js/rest'

await actorCalled('Apisit')
.whoCan(CallAnApi.at('https://serenity-js.org/'))
.attemptsTo(
List.of([
'/handbook/design/assertions',
'/handbook/design/screenplay-pattern'
]).
forEach(({ actor, item }) =>
actor.attemptsTo(
checkUrl(item),
))
)

Web assertions​

Interactions to Ensure.that and Ensure.eventually are interface-agnostic, so you can use them to verify interactions with REST APIs, mobile apps, web UIs, and so on.

Consider the following example web widget:

<h1 class="heading">Hello Serenity!</h1>

To interact with such widget, you'd define a PageElement describing how to locate it:

import { PageElement, By } from '@serenity-js/web'

const heading = () =>
PageElement.located(By.css('.heading'))

Since PageElement is an implementation of the standard Serenity/JS Question interface, it is accepted by the interaction to Ensure just like any other Answerable value:

Example web scenario interacting with the widget
import { actorCalled } from '@serenity-js/core'
import { Ensure } from '@serenity-js/assertions'
import { isVisible } from '@serenity-js/web'

await actorCalled('Edna').attemptsTo(
Ensure.that(heading(), isVisible()),
)

Fault-tolerant assertions​

What makes web testing challenging is having to deal with unpredictable delays typically caused by network traffic or complex animations.

To help you work around that, Serenity/JS offers an interaction to Ensure.eventually, which instead of failing the scenario immediately when the expectation is not met, instructs the actor to evaluate the actual value until it meets the expectation, or the interaction timeout expires.

Example web scenario interacting with the widget
import { actorCalled } from '@serenity-js/core'
import { Ensure } from '@serenity-js/assertions'
import { isVisible } from '@serenity-js/web'

await actorCalled('Edna').attemptsTo(
Ensure.eventually(heading(), isVisible()),
)

Note that while you can set the global interaction timeout in Serenity/JS configuration, you can also configure it for the specific assertion:

Example web scenario interacting with the widget
import { actorCalled, Duration } from '@serenity-js/core'
import { Ensure } from '@serenity-js/assertions'
import { isVisible } from '@serenity-js/web'

await actorCalled('Edna').attemptsTo(
Ensure.eventually(heading(), isVisible())
.timeoutAfter(Duration.ofMilliseconds(500)),
)

Implementing custom expectations​

Serenity/JS assertions and web modules provide expectations you'll need to implement even the most sophisticated test scenarios.

However, you can also implement custom expectations when needed. To do that, consult the examples in Expectation API docs and the Serenity/JS code base on GitHub.