UI自動(dòng)化測(cè)試,你只知道頁(yè)面對(duì)象(Page Object)模型嗎?

以前用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è)主要的好處:

  1. 將所有頁(yè)面元素選擇器保存在一個(gè)地方
  2. 標(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ù)真的很好用哎!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容