有几种方法可以做到这一点,具体取决于哪些参数是强制性的,哪些是可选的,以及您希望任务拥有多少个参数。
无参数
如果您有一个没有参数的任务,定义它的更简单方法是使用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)
,但第一种实现使用空字符串的默认值作为密码,而第二种实现甚至不触及密码字段。
我希望这有帮助!
简