首页 > 解决方案 > 如何使用两个参数制作任务?

问题描述

在 Serenity-js 书中,我们有一个只有一个参数的任务示例:

// spec/screenplay/tasks/add_a_todo_item.ts
import { PerformsTasks, Task } from 'serenity-js/protractor';

export class AddATodoItem implements Task {

static called(itemName: string) {                       // static method to improve the readability
    return new AddATodoItem(itemName);
}

performAs(actor: PerformsTasks): PromiseLike<void> {    // required by the Task interface
    return actor.attemptsTo(                            // delegates the work to lower-level tasks
        // todo: interact with the UI
    );
}

constructor(private itemName: string) {                 // constructor assigning the name of the item
                                                        // to a private field
}

想象一下,您可以添加一个 TodoItem 应该完成的日期。我们会收到一个日期参数,比如“截止日期”。我不知道该怎么做。

第一个想法:

构造函数:

constructor(private itemName: string, private deadline: Date) {
}

performAs:只需添加交互以键入截止日期

我们将有第二个静态方法。并且可能会更改被调用的方法返回。

感谢您的解释。

标签: serenity-js

解决方案


有几种方法可以做到这一点,具体取决于哪些参数是强制性的,哪些是可选的,以及您希望任务拥有多少个参数。

无参数

如果您有一个没有参数的任务,定义它的更简单方法是使用Task.where 工厂函数

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

const Login = () => Task.where(`#actor logs in`,
    Click.on(SubmitButton),
);

这与使用下面的类样式定义几乎相同,但代码要少得多:

class Login extends Task {
    performAs(actor: PerformsTasks) {
        return actor.attemptsTo( 
            Click.on(SubmitButton),
        );
    }

    toString() {
        return `#actor logs in`;
    }
}

一个参数

您可以将上述方法用于应接收一个参数的任务:

const LoginAs = (username: string) => Task.where(`#actor logs in as ${ username }`, 
    Enter.theValue(username).into(UsernameField),
    Click.on(SubmitButton),
);

或者,您也可以按如下方式实现:

const Login = {
  as: (username: string) => Task.where(`#actor logs in as ${ username }`, 
      Enter.theValue(username).into(UsernameField),
      Click.on(SubmitButton),
  ),
}

我发现第二个版本更优雅,更符合内置交互,如Click.on,Enter.theValue等,因为您将调用Login.as而不是LoginAs在您的演员流程中。

N个参数

如果有超过 1 个参数,但所有参数都是必需的,并且您只是追求优雅的 DSL,则可以将上述模式扩展如下:

const Login = {
    as: (username: string) => ({
        identifiedBy: (password: string) => Task.where(`#actor logs in as ${ username }`, 
            Enter.theValue(username).into(...),
            Enter.theValue(password).into(...),
            Click.on(SubmitButton),
    }),
}

然后,您将调用上述任务:

actor.attemptsTo(
    Login.as(username).identifiedBy(password),
);

这种设计不是特别灵活,因为它不允许您更改参数的顺序(即您不能说Login.identifiedBy(password).as(username))或使某些参数可选,但以相对较少的实现工作为您提供了一个好看的 DSL。

更大的灵活性

如果您需要更大的灵活性,例如在某些参数是可选的情况下,您可能会选择类样式定义和准构建器模式。(我说“准”是因为它不会改变对象,而是产生新对象)。

例如,假设虽然系统要求提供用户名,但密码可能是可选的。

class Login extends Task { 
    static as(username: string) {
        return new Login(username);
    }

    identifiedBy(password: string {
        return new Login(this.username, password);
    }

    constructor(
        private readonly username: string,
        private readonly password: string = '',
    ) {
        super();
    }

    performAs(actor: PerformsTasks) {
        return actor.attemptsTo(
            Enter.theValue(username).into(...),
            Enter.theValue(password).into(...),
            Click.on(SubmitButton),
        );
    }

    toString() {
        return `#actor logs in as ${ this.username }`;
    }
}

当然,您可以更进一步,将实例化任务的行为与任务本身分离,如果不同的任务足够不同以证明单独的实现是合理的,这很有用:

export class Login { 
    static as(username: string) {
        return new LoginWithUsernameOnly(username);
    }
}

class LoginWithUsernameOnly extends Task {

    constructor(
        private readonly username: string,
    ) {
        super();
    }

    identifiedBy(password: string {
        return new LoginWithUsernameAndPassword(this.username, password);
    }


    performAs(actor: PerformsTasks) {
        return actor.attemptsTo(
            Enter.theValue(username).into(...),
            Click.on(SubmitButton),
        );
    }

    toString() {
        return `#actor logs in as ${ this.username }`;
    }
}

class LoginWithUsernameAndPassword extends Task {

    constructor(
        private readonly username: string,
        private readonly username: string,
    ) {
        super();
    }


    performAs(actor: PerformsTasks) {
        return actor.attemptsTo(
            Enter.theValue(this.username).into(...),
            Enter.theValue(this.password).into(...),
            Click.on(SubmitButton),
        );
    }

    toString() {
        return `#actor logs in as ${ this.username }`;
    }
}

上述两种实现都允许您将任务称为Login.as(username)Login.as(username).identifiedBy(password),但第一种实现使用空字符串的默认值作为密码,而第二种实现甚至不触及密码字段。

我希望这有帮助!


推荐阅读