以前用Selenium做UI自動(dòng)化測(cè)試,接觸到了頁(yè)面對(duì)象也就是Page Object模型,它被稱為是selenium自動(dòng)化測(cè)試項(xiàng)目開發(fā)最佳測(cè)試設(shè)計(jì)模式,主要體現(xiàn)在對(duì)界面交互細(xì)節(jié)的封裝,這樣使得測(cè)試案例更加注重頁(yè)面而不是界面細(xì)節(jié),提高了測(cè)試用例的可讀性。
當(dāng)然了,能夠進(jìn)行e2e測(cè)試的框架有很多,除了selenium也有許多其他優(yōu)秀的測(cè)試框架。于是在學(xué)習(xí)和使用Cypress這樣一款測(cè)試框架時(shí),在了解基本使用語(yǔ)法,并寫了一些demo測(cè)試用例之后,很自然的想到,Page Object模型是否也可以運(yùn)用在Cypress上,并且也找到了一篇文章:
Deep diving PageObject pattern and using it with Cypress
看起來(lái)也是可以用的。
之后又看到Cypress官網(wǎng)博客上的另外一篇文章:
Stop using Page Objects and Start using App Actions
標(biāo)題是停止使用頁(yè)面對(duì)象,并使用app操作,看來(lái)作者并不建議使用Page Objects,這引起了我濃重的好奇心:為什么呢?不妨來(lái)跟我一起看看作者的觀點(diǎn)吧!
原文比較長(zhǎng),本文選擇了作者的主要觀點(diǎn),并翻譯評(píng)論之,如有疏漏歡迎指出。
Page objects頁(yè)面模型
通常測(cè)試人員會(huì)在web頁(yè)面的頂部創(chuàng)建另一層稱為page objects的中間層,用來(lái)執(zhí)行一些操作。在這篇文章中,作者認(rèn)為頁(yè)面對(duì)象是一種不好的實(shí)踐,并且建議將操作分派到應(yīng)用程序的內(nèi)部邏輯。
頁(yè)面模型視圖使得end-to-end(端到端)測(cè)試可讀并且易于管理。測(cè)試使用表示頁(yè)面用戶界面的實(shí)例來(lái)控制頁(yè)面,而不是與頁(yè)面進(jìn)行特別的交互。
頁(yè)面模型有兩個(gè)主要的好處:
- 將所有頁(yè)面元素選擇器保存在一個(gè)地方
- 標(biāo)準(zhǔn)化了測(cè)試與頁(yè)面的交互方式
Martin Fowler在他的PageObject文章中將頁(yè)面對(duì)象描述為HTML之上的另一個(gè)API。從概念上講,它們位于HTML之上。
Tests
-----------------
Page Objects
~ ~ ~ ~ ~ ~ ~ ~ ~
HTML UI
-----------------
Application code
上圖中的4層有3個(gè)不同密度的界面。
- 應(yīng)用程序代碼到HTML是緊密的
- HTML到頁(yè)面對(duì)象非常松散
- 對(duì)頁(yè)面對(duì)象的測(cè)試是緊密的
在Cypress中使用Page objects
你可以很輕松的在Cypress中使用Page objects,作者在文中也給了一些示例,比如一個(gè)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;
當(dāng)為“Homepage”編寫測(cè)試時(shí),我們可以重用來(lái)自另一個(gè)page對(duì)象的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;
這是一個(gè)典型的場(chǎng)景——您必須編寫一個(gè)完整的PageObject類層次結(jié)構(gòu),其中頁(yè)面的部分使用不同的頁(yè)面對(duì)象,并使用面向?qū)ο蟮脑O(shè)計(jì)組合它們。一個(gè)典型的測(cè)試是這樣的。
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
});
如果不用面向?qū)ο蟮腜ageObject實(shí)現(xiàn)呢?
你可以將典型的邏輯轉(zhuǎn)移到可重用的Cypress定制命令中,這些命令沒(méi)有任何內(nèi)部狀態(tài),只允許重用代碼。例如,可以實(shí)現(xiàn)一個(gè)“l(fā)ogin”命令。
// 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()
})
添加自定義命令后,測(cè)試可以像使用任何內(nèi)置命令一樣使用它。
// cypress/integration/spec.js
it('logs in', () => {
cy.visit('/login')
cy.login('username', 'password')
})
請(qǐng)注意,您不必總是創(chuàng)建自定義命令,簡(jiǎn)單的JavaScript函數(shù)也可以工作得很好(如果不是更好的話,因?yàn)轭愋蜋z查步驟可以理解單個(gè)函數(shù)簽名)。
// 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')
})
不知道各位看完上面這一大段有什么感覺(jué)?反正給我的初步感覺(jué)就是,使用Cypress的自定義命令,這種方式比Page Object要簡(jiǎn)潔的多!也許你不是特別熟悉Cypress,但是僅僅從代碼的長(zhǎng)度上應(yīng)該也有一些直觀的感受。
我也立刻嘗試使用這種方式寫了一些測(cè)試,果然很贊。那種感覺(jué),怎么說(shuō)呢,就像使用java和python來(lái)實(shí)現(xiàn)一個(gè)并不復(fù)雜的小功能,雖然兩種語(yǔ)言都可以寫,但是用了python之后感覺(jué)更舒爽,更順手。
但是作者的文章還沒(méi)有完,我們繼續(xù)看他的觀點(diǎn)。
Page objects的問(wèn)題
- Page objects很難維護(hù),并且占用了實(shí)際應(yīng)用程序開發(fā)的時(shí)間。我從來(lái)沒(méi)有見過(guò)PageObjects文檔化得足夠好,可以真正幫助編寫測(cè)試。
- Page objects將額外的狀態(tài)引入到測(cè)試中,測(cè)試與應(yīng)用程序的內(nèi)部狀態(tài)是分離的。這使得理解測(cè)試和失敗變得更加困難。
- Page objects試圖在一個(gè)統(tǒng)一的接口中適應(yīng)多個(gè)情況,回到條件邏輯——在我們看來(lái),這是一個(gè)巨大的反模式。
- Page objects使測(cè)試變慢,因?yàn)樗鼈兤仁箿y(cè)試始終通過(guò)應(yīng)用程序用戶界面。
不要絕望!我還將展示一個(gè)頁(yè)面對(duì)象的替代品,我稱之為“Application Actions(應(yīng)用程序操作)”,我們的端到端測(cè)試可以使用它。我相信應(yīng)用程序操作很好地解決了上述問(wèn)題,使端到端測(cè)試快速且高效。
后面作者還有介紹了一些具體的例子,在這些例子中,PageObject模式與我們編寫好的端到端測(cè)試所需的內(nèi)容之間存在差距。本文就不再贅述,感興趣可以去看原文。我們來(lái)看看作者所說(shuō)的Application Actions是個(gè)啥東東。
Application Actions應(yīng)用程序操作
想象一下,我們可以直接從測(cè)試中設(shè)置應(yīng)用程序的狀態(tài),而不是總是通過(guò)UI輸入新項(xiàng)。因?yàn)镃ypress體系結(jié)構(gòu)允許與測(cè)試中的應(yīng)用程序交互,所以這很簡(jiǎn)單。我們所需要做的就是暴露對(duì)應(yīng)用程序模型對(duì)象的引用。
作者文中也有給出具體的例子,并且指出這種方式運(yùn)行的更快。
Just functions僅僅是函數(shù)
使用應(yīng)用程序操作就是使用JavaScript函數(shù),使用函數(shù)很簡(jiǎn)單。
然后又給出了一些很棒的示例,同樣不再贅述。
下面我們?cè)偃タ匆幌?,運(yùn)用Application Actions時(shí)的一些限制。
Application actions limitations應(yīng)用程序操作限制
調(diào)用太多動(dòng)作太快
當(dāng)使用app操作執(zhí)行多個(gè)操作時(shí),您的測(cè)試可能會(huì)在應(yīng)用程序之前運(yùn)行。測(cè)試完成后,所有項(xiàng)目可能有時(shí)通過(guò),有時(shí)失敗。這都是因?yàn)闇y(cè)試運(yùn)行得比應(yīng)用程序處理操作的速度快。
通過(guò)使用app actions來(lái)驅(qū)動(dòng)應(yīng)用程序,我們改變了用戶使用應(yīng)用程序的方式。在頁(yè)面向用戶顯示項(xiàng)之前,用戶無(wú)法切換項(xiàng)。
作者強(qiáng)烈推薦這樣的模式——執(zhí)行一個(gè)應(yīng)用程序操作,通過(guò)編寫斷言等待UI更新到所需的狀態(tài),然后執(zhí)行另一個(gè)應(yīng)用程序操作,再次等待UI更新。這將盡可能快地運(yùn)行,因?yàn)镃ypress可以直接觀察DOM,并在斷言傳遞之后繼續(xù)下一步操作。
總結(jié):從測(cè)試中調(diào)用應(yīng)用程序操作的速度可能比應(yīng)用程序處理它們的速度要快。在這種情況下,由于測(cè)試和應(yīng)用程序之間的競(jìng)爭(zhēng),您可能會(huì)將測(cè)試解釋為脆弱的。幸運(yùn)的是,您可以通過(guò)幾種方式同步測(cè)試和應(yīng)用程序。測(cè)試可以:
- 等待DOM按預(yù)期更新。
- 觀察網(wǎng)絡(luò)流量,等待預(yù)期的XHR調(diào)用。
- 監(jiān)視應(yīng)用程序中的方法,并在調(diào)用該方法時(shí)繼續(xù)。
Actions是受限制的
有時(shí)應(yīng)用程序代碼無(wú)法實(shí)現(xiàn)所需的操作。例如,在Cypress最佳實(shí)踐的演講中,Brian Mann認(rèn)為:
- 當(dāng)測(cè)試登錄頁(yè)面時(shí),端到端測(cè)試應(yīng)該像用戶一樣使用UI
- 當(dāng)測(cè)試任何其他需要登錄的用戶流時(shí),測(cè)試應(yīng)該直接執(zhí)行登錄(例如使用cy.request()命令),而不是一次又一次地遍歷UI。
在上面的實(shí)現(xiàn)中,應(yīng)用程序代碼不能使用與cy.request相同的方法進(jìn)行登錄。因此,端到端測(cè)試應(yīng)該調(diào)用cy.request(),而不是調(diào)用應(yīng)用程序操作。這仍然避免使用page對(duì)象模式——自定義命令或簡(jiǎn)單的函數(shù)就足以實(shí)現(xiàn)它。
Final thoughts 最終想法
從始終通過(guò)頁(yè)面用戶界面的頁(yè)面對(duì)象切換到通過(guò)其內(nèi)部模型API控制應(yīng)用程序的應(yīng)用程序操作會(huì)帶來(lái)很多好處。
- 測(cè)試變得更快。即使是在Cypress的電子瀏覽器上本地運(yùn)行的簡(jiǎn)單TodoMVC測(cè)試,在從用戶界面切換到使用應(yīng)用程序操作之后,也從34秒增加到了17秒,速度提高了50%。
- 測(cè)試現(xiàn)在影響并受益于重構(gòu)應(yīng)用程序的代碼。應(yīng)用程序的內(nèi)部接口變得越合理和文檔化越好,就越容易為它們編寫端到端測(cè)試。
- 避免在短暫且不穩(wěn)定的用戶界面上編寫松散耦合的獨(dú)立代碼層。相反,測(cè)試使用并綁定到應(yīng)用程序更持久的內(nèi)部模型接口。
實(shí)際上,我只需要編寫將測(cè)試語(yǔ)法映射到應(yīng)用程序操作的實(shí)用程序函數(shù),其中大多數(shù)只是無(wú)狀態(tài)語(yǔ)法糖。
沒(méi)有并行狀態(tài)(頁(yè)面對(duì)象內(nèi)部),沒(méi)有條件測(cè)試邏輯——只是直接調(diào)用應(yīng)用程序代碼,就像您可以從DevTools控制臺(tái)做的那樣。
我再來(lái)嘮兩句
原文提供了很多具體的例子,并與頁(yè)面模型進(jìn)行比較??次疫@篇文章只能得到簡(jiǎn)要的概念,而仔細(xì)閱讀原文這些示例會(huì)幫助你理解作者的觀點(diǎn)。所以對(duì)此感興趣的朋友,強(qiáng)烈建議去讀一讀原文。
由于selenium的使用率比較廣,國(guó)內(nèi)許多書籍和博客文章都有相關(guān)資料,因此不少人一提到e2e,UI自動(dòng)化測(cè)試,只能想到selenium這么一個(gè)開源工具。由于UI界面變化比較快,為了方便編寫用例,有人提出了Page Object模型,并且也在實(shí)踐中被證明十分有用。
不過(guò)新的技術(shù)總是層出不窮的,多去了解了解更多的東西,開拓視野總沒(méi)有壞處。何況新的技術(shù)真的很好用哎!