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:
- the "actual value" - an
Answerable
to be evaluated in the context of the given actor, - an
Expectation
that defines the condition to be met by the actual value.
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:
- Serenity/JS assertions module provides general use expectations for any type of test automation
- Serenity/JS web module provides expectations dedicated to automating interactions with web interfaces
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:
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.
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:
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.