How to override task description? #2472
-
I like to provide informative descriptions to my tasks. I was successfully using toString() method before, the provided description would then be printed to reports and it could have been constructed when all the class variables were initialized. This no longer works. How could I override the message provided at the constructor later in the class? Illustrative example:
Here in the constructor i do not yet know the I did some digging arround and found that
this below works, but makes some duplication
|
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Dynamic descriptions, introduced in Serenity/JS 3.24, separate the static and dynamic descriptions of the activities. The "static" description is determined right before the actor performs a given activity by calling The "dynamic" description is enabled by the newly introduced The quick and dirty approachTo override the dynamic description, you could provide the static description when invoking class Modify extends Task {
static basket() {
return new Modify();
}
add(item) {
this.item = item;
return this;
}
constructor() {
super('#actor does something'); // static description
}
override describedBy(actor: Actor): Promise<string> {
return actor.answer(the`#actor adds ${this.item} to the basket`)
}
performAs(actor) { }
} While this works, it might result in the In your post, you allude to a fairly sophisticated DSL:
Let's then explore other design options. The recommended approachThe recommended approach when developing custom activities is to use import { Task, Answerable, the } from '@serenity-js/core';
// inheritance-based implementation
class AddItem extends Task {
constructor(private readonly item: Answerable<string>) {
super(the`#actor adds ${ item } to basket`);
}
}
// alternative, functional style implementation
const AddItem = (item: Answerable<string>) =>
Task.where(the`#actor adds ${ item } to basket`, /* ... */), However, this design approach requires the parameter reference (i.e. In your case, when the activity is instantiated, we don't know the value of the Separating configuration from instantiationOne approach to solve it could be to separate the problem of parameterising the activity from instantiating the activity. Let's then introduce a class called Implementing the Factory Method design patternIf the activity to add an item to the basket needs only one parameter, we can implement it following the factory method design pattern: class Basket {
static addItem(item: Answerable<string>) {
return new AddItem(item);
}
} Here, the static factory method Instantiating activities with multiple required parametersIf the activity to add an item required two or more parameters, say class Basket {
static addItem(item: Answerable<string>, quantity: Answerable<number> = 1) {
return new AddItem(item, quantity);
}
} This design works fine as long as the number of parameters is low and most or all of them are required. Configuring the Factory Method with an Options ObjectTo extend our design to support instantiating activities with a variable number of parameters, we could modify the To implement an options object pattern, we'd introduce an interface like interface AddItemOptions {
item: Answerable<string>; // required
quantity?: Answerable<number>; // optional
} We'd then modify the class Basket {
static addItem({ item, quantity = 1 }: AddItemOptions) {
return new AddItem(item, quantity);
}
} Configuring the Factory Method with a BuilderAn alternative to the Options Object pattern is to implement the Builder pattern. To do that, we'd introduce the basic builder interface, like this: interface ActivityBuilder {
build(): Activity
} Then, assuming that the different activities an actor could perform with the basket required different sets of options, we could have activity-specific builders: class Item implements ActivityBuilder {
private quantity: Answerable<number> = 1;
static called(name: Answerable<string>) {
return new Item(name);
}
constructor(private readonly name: Answerable<string>) {
}
twice() {
this.quantity = 2;
return this;
}
times(quantity: Answerable<number>) {
this.quantity = quantity;
return this;
}
build() {
return Task.where(the`#actor adds ${ quantity } x ${ this.name } to the basket`, /* */)
}
} Finally, we'd modify the class Basket {
add(item: Item): Activity {
return item.build();
}
} This design enables quite a nice modular DSL: await actor.attemptsTo(
Basket.add(Item.called('pie').twice()),
) |
Beta Was this translation helpful? Give feedback.
Dynamic descriptions, introduced in Serenity/JS 3.24, separate the static and dynamic descriptions of the activities.
The "static" description is determined right before the actor performs a given activity by calling
activity.toString(): string
method, where the returned value is included in theActivityStarts
event. This behaviour is consistent with Serenity/JS pre-3.24.The "dynamic" description is enabled by the newly introduced
Describable
interface, which allows the actor to retrieve the description of the activity right after it has been performed and when the values of its parameters are already resolved. This is done by callingactivity.describedBy(actor): Promise<string>
method, …