我们在之前的干净代码文章中涵盖了一些关于干净代码的建议。然而,干净代码的建议列表不可能轻松地进行总结。因此,这可以被视为前一篇文章的实用续集。这些原则是编程中使用的东西。在测试自动化中同样应该应用SOLID原则。SOLID是一个首字母缩略词,每个字母代表一个原则:S – 单一责任原则(Single Responsibility Principle),O – 开闭原则(Open Closed Principle),L – 里氏替换原则(Liskov Substitution Principle),I – 接口隔离原则(Interface Segregation Principle),D – 依赖倒置原则(Dependency Injection Principle)。我们将通过针对测试自动化的具体示例逐一介绍这些原则。
单一责任原则(SRP)
单一责任原则的本质在于,编程中的一个模块应该只有一个目的。替代性的定义说,一个模块应该只有一个变更的理由。就所有意图而言,我们将模块视为类、方法、测试等。在测试自动化中,当我们使用**页面对象模型(Page Object Model, POM)**时,我们遵循了SRP。在POM中,我们创建包含仅属于一个页面的元素的类。如果我们混合不同页面的元素,这将意味着我们违反了SRP。
然而,如果你像我一样,喜欢为每个控件创建特定的类,这些类应包含所有可以与该元素一起使用的方法,那么你必须更加小心。让我们看一个使用Playwright和TypeScript的示例:
// solid principles in test automation
这是一个可重用的类,可以用于应用程序中的任何按钮。这个类扩展了BaseControl类,BaseControl类包含查找元素的函数。元素定位器从页面对象类传递,在那里我创建按钮类的实例并将定位器作为参数传递。
按钮类包含三个与按钮元素相关的方法。如果我在这里添加任何其他元素(例如下拉菜单)的方法,我将违反SRP。
SRP也可以应用于测试。一个测试应该只测试一件事的前提正是SRP的核心。如果你的测试中有多个断言,你就违反了SRP。
开闭原则(OCP)
**开闭原则(Open Closed Principle, OCP)**是测试自动化中最重要的SOLID原则之一。它规定,一个模块应该对扩展开放,对修改关闭。实际上,我们可以说,一个工作中的代码类可以被其他类扩展(继承),但我们不应该修改其内容。修改会导致破坏性变化。通过扩展,我们向代码库添加新代码,但不触碰现有代码。
在我们上面的按钮类示例中,有一些事情我不被允许去做。例如,如果我在被测试系统中发现一个新的按钮,它不能与现有的点击函数一起工作,我可以修改该函数以使其工作。这将是错误的选择,因为我将违反OCP。在这种情况下,正确的做法是将按钮函数抽象到一个接口中,并在实现该接口的类中实现每个函数的逻辑。
export interface ButtonInterface {
hover(): Promise<void>;
click(): Promise<void>;
isHovered(): Promise<boolean> | undefined;
}
export class Button implements ButtonInterface {
hover(): Promise<void> {
//函数实现
}
click(): Promise<void> {
//函数实现
}
isHovered(): Promise<boolean> | undefined {
//函数实现
}
}
export class NewButton implements ButtonInterface {
hover(): Promise<void> {
//函数实现
}
click(): Promise<void> {
//函数实现
}
isHovered(): Promise<boolean> | undefined {
//函数实现
}
}
另一种方法是创建一个新类,扩展按钮类并在新类中重写点击函数。然后,在需要表示这种类型按钮的页面对象类中创建这个新类的实例。
在这个类中添加新函数是否被视为违反OCP?这取决于许多因素。新代码是否干扰现有测试?它是否需要因为这个新函数而更改基类中的某些内容?如果答案是肯定的,那么OCP将被打破。
里氏替换原则(Liskov Substitution Principle)
里氏替换原则(Liskov Substitution Principle, LSP)是对OCP的一种补充。它指出,我们可以在测试中用派生类替换基类,并保持现有功能不变。在我们上面的按钮类示例中,我们应该能够用按钮类替换BaseControl类的实例,而不会破坏测试的功能。
export class SomeClass {
constructor(controlProperties: IControlProperties) {
this.controlProperties = controlProperties;
}
let control = new BaseControl();
let element = control.findControl(this.controlProperties);
}
// 这应该同样有效
export class SomeClass {
constructor(controlProperties: IControlProperties) {
this.controlProperties = controlProperties;
}
// 我们用Button替换BaseControl
let control = new Button();
let element = control.findControl(this.controlProperties);
}
接口隔离原则(Interface Segregation Principle)
**接口隔离原则(Interface Segregation Principle, ISP)**帮助我们避免过度实现我们不需要使用的接口。拥有包含许多函数的大接口,这些函数在每个实现中并不总是需要,是我们不想要的。我们通常希望拥有更小的接口,包含特定的函数。在下面(不好的)示例中,我们有一个单一的控件接口,涵盖了我们应用程序中可能拥有的几种类型的控件:按钮、输入字段和值列表。因此,这个接口中有typeText
、click
和getText
方法。实现这个接口的类需要实现每个方法,尽管按钮类不需要typeText
和getText
方法。按钮不能具有这样的功能。
export interface ControlInterface {
typeText(): Promise<void>;
click(): Promise<void>;
getText(): Promise<string>;
}
export class Button implements ControlInterface {
typeText(): Promise<void> {
//不需要
}
click(): Promise<void> {
//函数实现
}
getText(): Promise<string> {
//不需要
}
}
export class TextField implements ControlInterface {
typeText(): Promise<void> {
//函数实现
}
click(): Promise<void> {
//不需要
}
getText(): Promise<string> {
//函数实现
}
}
解决方案是分离这个接口,在每个特定接口中只添加它所需的方法。然后,每个控件类如Button和TextField只实现它们需要的接口。在有些情况下,比如下拉控件可能需要所有三个方法click
、typeText
和getText
,我们可以实现多个接口。
依赖倒置原则(Dependency Inversion Principle)
依赖倒置原则(Dependency Inversion Principle, DIP)最好与依赖注入一起使用。DIP主张软件模块不应该依赖于具体的实现(如类),而应该依赖于抽象。通过这种方式,我们消除了代码各部分之间的紧密耦合,使代码片段更易于替换且不易出错。这一SOLID原则在测试自动化中最著名的例子是Selenium中WebDriver接口的使用。WebDriver是一个接口,由ChromeDriver、FirefoxDriver和EdgeDriver类实现。这些类的实现仅与特定浏览器相关。
public class HomePage {
private WebDriver driver;
public HomePage(WebDriver driver) {
this.driver = driver;
}
public String getTitleText() {
return driver.findElement(By.id("title")).getText();
}
public void closeButtonClick() {
driver.findElement(By.id("button")).click();
}
}
@Test
public class HomePageTest {
WebDriver driver = new ChromeDriver();
HomePage homepage = new HomePage(driver);
Assert.assertEquals(homepage.getTitleText(), "Some page title", "Wrong title");
}
如上面的代码所示,ChromeDriver的实现被注入到HomePage构造函数中。这使得类依赖于抽象而不是具体的驱动实现。
结论
SOLID原则在测试自动化中与在应用程序编程中同样重要。测试也必须遵循最佳编码实践。通过对SOLID原则的良好理解,我们可以避免将来可能会花费我们大量时间和资源的错误。可以理解,有些情况是不可预见的,我们无法避免一些架构上的错误和延迟。然而,借助SOLID,我们可以最小化未知对我们测试的影响。这些原则并不容易理解和实现,但实践出真知。在经历了多个实施这些原则的情境后,我们可以对大多数情境中的概念有更高的理解。