以前用Selenium做UI自动化测试,接触到了页面对象也就是Page Object模型,它被称为是selenium自动化测试项目开发最佳测试设计模式,主要体现在对界面交互细节的封装,这样使得测试案例更加注重页面而不是界面细节,提高了测试用例的可读性。
当然了,能够进行e2e测试的框架有很多,除了selenium也有许多其他优秀的测试框架。于是在学习和使用Cypress这样一款测试框架时,在了解基本使用语法,并写了一些demo测试用例之后,很自然的想到,Page Object模型是否也可以运用在Cypress上,并且也找到了一篇文章:
Deep diving PageObject pattern and using it with Cypress
看起来也是可以用的。
之后又看到Cypress官网博客上的另外一篇文章:
Stop using Page Objects and Start using App Actions
标题是停止使用页面对象,并使用app操作,看来作者并不建议使用Page Objects,这引起了我浓重的好奇心:为什么呢?不妨来跟我一起看看作者的观点吧!
原文比较长,本文选择了作者的主要观点,并翻译评论之,如有疏漏欢迎指出。
Page objects页面模型
通常测试人员会在web页面的顶部创建另一层称为page objects的中间层,用来执行一些操作。在这篇文章中,作者认为页面对象是一种不好的实践,并且建议将操作分派到应用程序的内部逻辑。
页面模型视图使得end-to-end(端到端)测试可读并且易于管理。测试使用表示页面用户界面的实例来控制页面,而不是与页面进行特别的交互。
页面模型有两个主要的好处:
- 将所有页面元素选择器保存在一个地方
- 标准化了测试与页面的交互方式
Martin Fowler在他的PageObject文章中将页面对象描述为HTML之上的另一个API。从概念上讲,它们位于HTML之上。
Tests
-----------------
Page Objects
~ ~ ~ ~ ~ ~ ~ ~ ~
HTML UI
-----------------
Application code
上图中的4层有3个不同密度的界面。
- 应用程序代码到HTML是紧密的
- HTML到页面对象非常松散
- 对页面对象的测试是紧密的
在Cypress中使用Page objects
你可以很轻松的在Cypress中使用Page objects,作者在文中也给了一些示例,比如一个SignInpage
类:
class SignInPage {
visit() {
cy.visit('/signin');
}
getEmailError() {
return cy.get(`[data-testid=SignInEmailError]`);
}
getPasswordError() {
return cy.get(`[data-testid=SignInPasswordError]`);
}
fillEmail(value) {
const field = cy.get(`[data-testid=SignInEmailField]`);
field.clear();
field.type(value);
return this;
}
fillPassword(value) {
const field = cy.get(`[data-testid=SignInPasswordField]`);
field.clear();
field.type(value);
return this;
}
submit() {
const button = cy.get(`[data-testid=SignInSubmitButton]`);
button.click();
}
}
export default SignInPage;
当为“Homepage”编写测试时,我们可以重用来自另一个page对象的SignInPage
:
import Header from './Headers';
import SignInPage from './SignIn';
class HomePage {
constructor() {
this.header = new Header();
}
visit() {
cy.visit('/');
}
getUserAvatar() {
return cy.get(`[data-testid=UserAvatar]`);
}
goToSignIn() {
const link = this.header.getSignInLink();
link.click();
const signIn = new SignInPage();
return signIn;
}
}
export default HomePage;
这是一个典型的场景——您必须编写一个完整的PageObject类层次结构,其中页面的部分使用不同的页面对象,并使用面向对象的设计组合它们。一个典型的测试是这样的。
import HomePage from '../elements/pages/HomePage';
describe('Sign In', () => {
it('should show an error message on empty input', () => {
const home = new HomePage();
home.visit();
const signIn = home.goToSignIn();
signIn.submit();
signIn.getEmailError()
.should('exist')
.contains('Email is required');
signIn
.getPasswordError()
.should('exist')
.contains('Password is required');
});
// more tests
});
如果不用面向对象的PageObject实现呢?
你可以将典型的逻辑转移到可重用的Cypress定制命令中,这些命令没有任何内部状态,只允许重用代码。例如,可以实现一个“login”命令。
// in cypress/support/commands.js
Cypress.Commands.add('login', (username, password) => {
cy.get('#login-username').type(username)
cy.get('#login-password').type(password)
cy.get('#login').submit()
})
添加自定义命令后,测试可以像使用任何内置命令一样使用它。
// cypress/integration/spec.js
it('logs in', () => {
cy.visit('/login')
cy.login('username', 'password')
})
请注意,您不必总是创建自定义命令,简单的JavaScript函数也可以工作得很好(如果不是更好的话,因为类型检查步骤可以理解单个函数签名)。
// cypress/integration/util.js
export const login = (username, password) => {
cy.get('#login-username').type(username)
cy.get('#login-password').type(password)
cy.get('#login').submit()
}
// cypress/integration/spec.js
import { login } from './util'
it('logs in', () => {
cy.visit('/login')
login('username', 'password')
})
不知道各位看完上面这一大段有什么感觉?反正给我的初步感觉就是,使用Cypress的自定义命令,这种方式比Page Object要简洁的多!也许你不是特别熟悉Cypress,但是仅仅从代码的长度上应该也有一些直观的感受。
我也立刻尝试使用这种方式写了一些测试,果然很赞。那种感觉,怎么说呢,就像使用java和python来实现一个并不复杂的小功能,虽然两种语言都可以写,但是用了python之后感觉更舒爽,更顺手。
但是作者的文章还没有完,我们继续看他的观点。
Page objects的问题
- Page objects很难维护,并且占用了实际应用程序开发的时间。我从来没有见过PageObjects文档化得足够好,可以真正帮助编写测试。
- Page objects将额外的状态引入到测试中,测试与应用程序的内部状态是分离的。这使得理解测试和失败变得更加困难。
- Page objects试图在一个统一的接口中适应多个情况,回到条件逻辑——在我们看来,这是一个巨大的反模式。
- Page objects使测试变慢,因为它们迫使测试始终通过应用程序用户界面。
不要绝望!我还将展示一个页面对象的替代品,我称之为“Application Actions(应用程序操作)”,我们的端到端测试可以使用它。我相信应用程序操作很好地解决了上述问题,使端到端测试快速且高效。
后面作者还有介绍了一些具体的例子,在这些例子中,PageObject模式与我们编写好的端到端测试所需的内容之间存在差距。本文就不再赘述,感兴趣可以去看原文。我们来看看作者所说的Application Actions是个啥东东。
Application Actions应用程序操作
想象一下,我们可以直接从测试中设置应用程序的状态,而不是总是通过UI输入新项。因为Cypress体系结构允许与测试中的应用程序交互,所以这很简单。我们所需要做的就是暴露对应用程序模型对象的引用。
作者文中也有给出具体的例子,并且指出这种方式运行的更快。
Just functions仅仅是函数
使用应用程序操作就是使用JavaScript函数,使用函数很简单。
然后又给出了一些很棒的示例,同样不再赘述。
下面我们再去看一下,运用Application Actions时的一些限制。
Application actions limitations应用程序操作限制
调用太多动作太快
当使用app操作执行多个操作时,您的测试可能会在应用程序之前运行。测试完成后,所有项目可能有时通过,有时失败。这都是因为测试运行得比应用程序处理操作的速度快。
通过使用app actions来驱动应用程序,我们改变了用户使用应用程序的方式。在页面向用户显示项之前,用户无法切换项。
作者强烈推荐这样的模式——执行一个应用程序操作,通过编写断言等待UI更新到所需的状态,然后执行另一个应用程序操作,再次等待UI更新。这将尽可能快地运行,因为Cypress可以直接观察DOM,并在断言传递之后继续下一步操作。
总结:从测试中调用应用程序操作的速度可能比应用程序处理它们的速度要快。在这种情况下,由于测试和应用程序之间的竞争,您可能会将测试解释为脆弱的。幸运的是,您可以通过几种方式同步测试和应用程序。测试可以:
- 等待DOM按预期更新。
- 观察网络流量,等待预期的XHR调用。
- 监视应用程序中的方法,并在调用该方法时继续。
Actions是受限制的
有时应用程序代码无法实现所需的操作。例如,在Cypress最佳实践的演讲中,Brian Mann认为:
- 当测试登录页面时,端到端测试应该像用户一样使用UI
- 当测试任何其他需要登录的用户流时,测试应该直接执行登录(例如使用cy.request()命令),而不是一次又一次地遍历UI。
在上面的实现中,应用程序代码不能使用与cy.request相同的方法进行登录。因此,端到端测试应该调用cy.request(),而不是调用应用程序操作。这仍然避免使用page对象模式——自定义命令或简单的函数就足以实现它。
Final thoughts 最终想法
从始终通过页面用户界面的页面对象切换到通过其内部模型API控制应用程序的应用程序操作会带来很多好处。
- 测试变得更快。即使是在Cypress的电子浏览器上本地运行的简单TodoMVC测试,在从用户界面切换到使用应用程序操作之后,也从34秒增加到了17秒,速度提高了50%。
- 测试现在影响并受益于重构应用程序的代码。应用程序的内部接口变得越合理和文档化越好,就越容易为它们编写端到端测试。
- 避免在短暂且不稳定的用户界面上编写松散耦合的独立代码层。相反,测试使用并绑定到应用程序更持久的内部模型接口。
实际上,我只需要编写将测试语法映射到应用程序操作的实用程序函数,其中大多数只是无状态语法糖。
没有并行状态(页面对象内部),没有条件测试逻辑——只是直接调用应用程序代码,就像您可以从DevTools控制台做的那样。
我再来唠两句
原文提供了很多具体的例子,并与页面模型进行比较。看我这篇文章只能得到简要的概念,而仔细阅读原文这些示例会帮助你理解作者的观点。所以对此感兴趣的朋友,强烈建议去读一读原文。
由于selenium的使用率比较广,国内许多书籍和博客文章都有相关资料,因此不少人一提到e2e,UI自动化测试,只能想到selenium这么一个开源工具。由于UI界面变化比较快,为了方便编写用例,有人提出了Page Object模型,并且也在实践中被证明十分有用。
不过新的技术总是层出不穷的,多去了解了解更多的东西,开拓视野总没有坏处。何况新的技术真的很好用哎!