Assertions

Serenity/JS does not force you to use any particular assertion library, so you can use any that supports promises.

The examples below assume that you're using chai with chai-as-promised.

import chai = require('chai');
chai.use(require('chai-as-promised'));

const expect = chai.expect;

Go with the flow

To execute an assertion as part of the actor's flow, we need to wrap it into a Task, such as the built-in one - to See:

import { See } from 'serenity-js/lib/screenplay';

See.if<T>(question: Question<T>, assertion: Assertion<T>);

For example, if we wanted to check if a specific item is present in a list of items displayed on the screen, defines as:

export class TodoList {
    static Items_Available = Text.ofAll(Target.the('items on the list').located(by.repeater('todo in todos')));
}

We could instruct the actor:

actor.attemptsTo(
    See.if(TodoList.Items_Available, items => expect(items).to.eventually.include('some item of interest')),
)

This works, but is not particularly readable. Let's improve it.

Aliasing

The name of the expected element from the above example:

items => expect(items).to.eventually.include.('some item of interest');

could be made configurable:

expected => actual => expect(items).to.eventually.include(expected);

We could also give the above function a name, and store it in our project as, say assertions.ts, so that it can be reused in other scenarios:

// assertions.ts
export include = expected => actual => expect(items).to.eventually.include(expected);

With this one-liner in place, we could improve the task definition from the previous example:

import { include } from './assertions.ts';

actor.attemptsTo(
    See.if(TodoList.Items_Available, include('some item of interest')),
)

Domain-specific assertions

The scenario where aliasing and domain-specific assertion can be very useful is in more complex domains, where you implement domain-specific questions. For example:

See.if(TradeConfirmation.Last_Trade, wasExecutedWithin(4, 'millis'))

Retaining semantic meaning

A regular chai assertion, such as this one:

expect(1 + 1).to.equal(2);

does not retain any semantic meaning at runtime. If it fails - it throws an error, but it it passes, you won't notice it.

However, audit or compliance reasons might require our tests to report that a given assertion has been performed.

In those scenarios, the task to See can be wrapped in any regular task, annotated with @step, so that it gets reported. For example:

class AccountILocked implements Task {

    @step('{0} ensures that \'#account\' is locked and can no longer be modified')
    performAs(actor: PerformsTasks): PromiseLike<void> {
        return actor.attemptsTo(
            See.if(AccountStatus.of(this.account), isMarkedAs('locked')),
        );
    }

    constructor(private account: string) {
    }
}

:bulb: PRO TIP: Wrapping an assertion into a task lets you execute it as part of any other task, for example as a precondition.

Delayed assertions

Sometimes an assertion needs to be performed against something that occured earlier on in the scenario and is no longer available.

For example, an actor is shown a popup with a voucher code that will be applied to their shopping basket should they choose to subscribe to a newsletter. When the actor is ready for checkout, we'd like to assert that the voucher they've been shown has been applied.

This scenario requires an actor, stateless by nature, to take note of what they've seen. For this reason, we need to give the actor the following ability:

import { TakeNotes } from 'serenity-js/lib/screenplay';

Actor.named('Benjamin').whoCan(TakeNotes.usingAnEmptyNotepad());

An actor, of course, can have many abilities:

Actor.named('Benjamin').whoCan(TakeNotes.usingAnEmptyNotepad(), BrowseTheWeb.using(protractor.browser), /* etc */);

With the ability to TakeNotes in place, and some questions they can ask defined:

export class MarketingPopup {
    static Voucher_Code = Text.of(Target.the('voucher code').located(by.id('newsletter-voucher')));
}

export class Checkout {
    static Applied_Vouchers = Text.ofAll(Target.the('applied vouchers').located(by.repeater('voucher in vouchers')))
}

and an include assertion like in the previous example, an actor can:

actor.attemptsTo(
    TakeNote.of(MarketingPopup.Voucher_Code),

    /* ... perform other tasks */

    CompareNotes.toSeeIf(Checkout.Applied_Vouchers, include, MarketingPopup.Voucher_Code),
);

Your feedback matters!

Suggest features and improvements on github, get in touch on twitter, and if you found Serenity/JS useful - don't forget to give it a star! ★

Star

results matching ""

    No results matching ""