Skip to main content

externalabstractTask

Tasks model sequences of activities and help you capture meaningful steps of an actor workflow in your domain.

Typically, tasks correspond to higher-level, business domain-specific activities like to BookAPlaneTicket, PlaceATrade, TransferFunds, and so on. However, higher-level tasks can and should be composed of lower-level tasks. For example, a task to SignUp could be composed of tasks to ProvideUsername and ProvidePassword.

The lowest-level tasks in your abstraction hierarchy should be composed of interactions. For example, a low-level task to ProvideUsername could be composed of an interaction to enter the value into a form field and press the Key.Enter.

Tasks are the core building block of the Screenplay Pattern, along with actors, abilities, interactions, and questions.

Learn more about:

Defining a task

import { Answerable, Task, the } from '@serenity-js/core'
import { By, Click, Enter, PageElement, Press, Key } from '@serenity-js/web'

const SignIn = (username: Answerable<string>, password: Answerable<string>) =>
Task.where(the`#actor signs is as ${ username }`,
Enter.theValue(username).into(PageElement.located(By.id('username'))),
Enter.theValue(password).into(PageElement.located(By.id('password'))),
Press.the(Key.Enter),
);

Defining a not implemented task

Note that calling Task.where method without providing the sequence of activities produces a Task that's marked as "pending" in the test report.

This feature is useful when you want to quickly write down a task that will be needed in the scenario, but you're not yet sure what activities it will involve.

import { Task, the } from '@serenity-js/core'

const SignUp = () =>
Task.where(the`#actor signs up for a newsletter`) // no activities provided
// => task marked as pending

Composing activities into tasks

The purpose of tasks is to help you capture domain vocabulary by associating domain meaning with a sequence of activities. From the implementation perspective, tasks help you give a meaningful description to such sequence and provide a way to easily reuse activities across your code base.

Remember

Tasks associate domain meaning with a sequence of lower-level activities and provide a mechanism for code reuse.

For example, a task to find a flight connection from London to New York could be modelled as a sequence of the following lower-level activities:

  • specify origin city of "London"
  • specify destination city of "New York"

The easiest way to implement such task, and any custom Serenity/JS task for this matter, is to use the Task.where method to compose the lower-level activities:

import { Task, the } from '@serenity-js/core'

const findFlight = (originCity: string, destinationCity: string) =>
Task.where(the`#actor finds a flight from ${ originCity } to ${ destinationCity }`, // task goal
specifyOriginCity(originCity), // activities
specifyDestinationCity(originCity),
)

Furthermore, if the actor was interacting with a web UI, a task to specify origin city could again be composed of other activities:

  • click on the origin airport widget
  • enter city name of London
  • pick the first suggested airport from the list

Conversely, a task to specify destination city could be composed of:

  • click on the destination airport widget
  • enter city name of New York
  • pick the first suggested airport from the list

Conveniently, Serenity/JS modules provide low-level activities that allow actors to interact with the various interfaces of the system under test. For example, Serenity/JS Web module ships with activities such as Click or Enter, which we can incorporate into our task definitions just like any other activities:

import { Task, the } from '@serenity-js/core'
import { Click, Enter, Key, Press } from '@serenity-js/web'

import { FlightFinder } from './ui/flight-finder'

const specifyOriginCity = (originCity: string) =>
Task.where(the`#actor specifies origin city of ${ originCity }`,
Click.on(FlightFinder.originAirport),
Enter.theValue(originCity).into(FlightFinder.originAirport),
Press.the(Key.ArrowDown, Key.Enter).into(FlightFinder.originAirport),
)

const specifyDestinationCity = (destinationCity: string) =>
Task.where(the`#actor specifies destination city of ${ destinationCity }`,
Click.on(FlightFinder.destinationAirport),
Enter.theValue(destinationCity).into(FlightFinder.destinationAirport),
Press.the(Key.ArrowDown, Key.Enter).into(FlightFinder.destinationAirport),
)

As you can already see, tasks to specify origin city and specify destination city are almost identical, save for the name of the widget and the text value the actor is supposed to enter. Serenity/JS task-based code reuse model means that we can clean up such duplicated implementation by extracting a parameterised task, in this case called specifyCity:

import { Task, the } from '@serenity-js/core'
import { Click, Enter, Key, PageElement, Press } from '@serenity-js/web'

import { FlightFinder } from './ui/flight-finder'

const specifyOriginCity = (originCity: string) =>
Task.where(the`#actor specifies origin city of ${ originCity }`,
specifyCity(originCity, FlightFinder.originAirport)
)

const specifyDestinationCity = (destinationCity: string) =>
Task.where(the`#actor specifies destination city of ${ destinationCity }`,
specifyCity(destinationCity, FlightFinder.destinationAirport),
)

const specifyCity = (cityName: string, widget: PageElement) =>
Task.where(the`#actor specifies city of ${ cityName } in ${ widget }`,
Click.on(widget),
Enter.theValue(cityName).into(widget),
Press.the(Key.ArrowDown, Key.Enter).into(widget),
)

As you work with Serenity/JS, you'll notice that the ideas of functional decomposition, so thinking of tasks as sequences of lower-level activities, as well as functional composition, so implementing reusable activities and composing them into higher-level tasks, are at the heart of the Screenplay Pattern. You'll also notice that the entire Serenity/JS framework does it best to help your team follow this approach.

The power of the Serenity/JS task-based code reuse model

What makes the Serenity/JS task-based code reuse model so powerful at scale is the observation that:

  • for most software systems, a vast number of diverse test scenarios can be composed of a relatively small number of high-level activities
  • all high-level activities can be composed of a relatively small number of lower-level activities
  • in a reasonably consistently-designed software system, most lower-level activities tend to be similar and rather consistent across the various aspects of a given interface. In particular, there are only so many ways one can interact with a UI button or send an HTTP request to a web service.

What this means in practice is that by investing your time in properly designing relatively few reusable tasks to test your system, you give your team a significant productivity boost and leverage when producing high-level test scenarios.

On top of that, this design approach results not only in simpler test scenarios that reduce the cognitive load on the reader as they require them to process the scenario only one level of abstraction at the time. It also allows for the test to take shortcuts in well-defined points of the workflow - use a REST API request to create a test user account instead of going through the registration form.

Hierarchy

Index

Constructors

externalconstructor

  • new Task(description: Answerable<string>, location?: FileSystemLocation): Task
  • Parameters

    • externaldescription: Answerable<string>
    • externallocation: FileSystemLocation = ...

    Returns Task

Methods

staticexternalwhere

  • A factory method that makes defining custom tasks more convenient.


    Parameters

    • externaldescription: Answerable<string>

      A description to be used when reporting this task

    • externalrest...activities: Activity[]

      A sequence of lower-level activities that constitute this task

    Returns Task

externalinstantiationLocation

  • instantiationLocation(): FileSystemLocation
  • Returns the location where this Activity was instantiated.


    Returns FileSystemLocation

externalabstractperformAs

externaldescribedBy

  • Resolves the description of this object in the context of the provided actor.


    Parameters

    Returns Promise<string>

externaltoString

  • toString(): string
  • Returns a human-readable description of this object.


    Returns string