京喜前端自動化測試之路

做者: 阿翔html

前言

京喜(原京東拼購)項目,做爲京東戰略級業務,擁有千萬級別的流量入口。爲了保障線上業務的穩定運行,每個月例行開展前端容災演習,主要包含小程序及 H5 版本,要求各頁面各模塊在異常狀況下進行適當的降級處理,不能出現空窗、樣式錯亂、不合理的錯誤提示等體驗問題。 原來的容災演習過程:小程序(通訊方式改爲 Https )和 H5 經過 Whistle 對接口返回進行修改來模擬異常狀況,驗證各頁面各模塊的降級處理符合預期。容災演習是一項長期持續的工做,且涉及頁面功能及場景多,人工的切換場景模擬異常致使演習效率很低,所以想經過開發自動化測試工具來提高研發效率,讓容災演習工做隨時能夠輕鬆開展。京喜 H5 和小程序場景差別比較大,所以自動化測試之路分 H5 和小程序兩部分進行,以 H5 做爲一個開篇。前端

綜上所述,咱們但願京喜 H5 自動化測試工具能夠提供如下功能:node

  1. 訪問目標頁面,對頁面進行截圖;
  2. 設置 UA(模擬不一樣渠道:微信、手Q、其它瀏覽器等);
  3. 模擬用戶點擊、滑動頁面操做;
  4. 網絡攔截、模擬異常狀況(接口響應碼 500、接口返回數據異常);
  5. 操做緩存數據(模擬有無緩存的場景等)。

技術選型

提到 Web 的自動化測試,不少人熟悉的是 Selenium 2.0(Selenium WebDriver), 支持多平臺、多語言、多款瀏覽器(經過各類瀏覽器的驅動來驅動瀏覽器),提供了功能豐富的API接口。而隨着前端技術的發展,Selenium 2.0 逐漸呈現出環境安裝複雜、API 調用不友好、性能不高等缺點。新一代的自動化測試工具 —— Puppeteer ,相較於 Selenium WebDriver 環境安裝更簡單、性能更好、效率更高、在瀏覽器執行 Javascript 的 API 更簡單,它還提供了網絡攔截等功能。git

Puppeteer 是一個 Node 庫,它提供了一套高階 API ,經過 Devtools 協議控制 ChromiumChrome 瀏覽器。Puppeteer 默認以 Headless 模式運行,可是能夠經過修改配置文件運行「有頭」模式。github

官方描述的功能:chrome

  • 生成頁面 PDF;
  • 抓取 SPA(單頁應用)並生成預渲染內容(即「 SSR 」,服務器端渲染);
  • 自動提交表單,進行 UI 測試,鍵盤輸入等;
  • 建立一個時時更新的自動化測試環境,使用 JavaScript 和最新的瀏覽器功能直接在最新版本的 Chrome 中執行測試;
  • 捕獲網站的 Timeline Trace,用來幫助分析性能問題;
  • 測試瀏覽器擴展。

Puppeteer 提供了一種啓動 Chromium 實例的方法。 當 Puppeteer 鏈接到一個 Chromium 實例的時候會經過 puppeteer.launch 或 puppeteer.connect 建立一個 Browser 對象,在經過 Browser 建立一個 Page 實例,導航到一個 Url ,而後保存截圖。一個 Browser 實例能夠有多個 Page 實例。 下面就是使用 Puppeteer 進行自動化的一個典型示例:npm

const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'screenshot.png'});
  await browser.close();
});
複製代碼

綜上所述,咱們選擇基於 Puppeteer 來開發京喜首頁容災演習的自動化測試工具,經過 Puppeteer 提供的一系列 API ,實現訪問目標頁面、模擬異常場景、生成截圖的過程自動化。最後再經過人工比對截圖,判斷頁面降級處理是否符合預期、用戶體驗是否友好。json

實現方案

咱們將容災演習過程分爲自動化流程和人工操做兩部分。小程序

自動化流程:api

  1. 模擬用戶訪問頁面操做;
  2. 攔截網絡請求,修改接口返回數據,模擬異常場景(接口返回 500、異常數據等);
  3. 生成截圖。

人工操做:

自動化腳本執行完畢後,人工比對各個場景的截圖,判斷是否符合預期。

方案流程圖:

方案流程圖

開發實錄

安裝 Puppeteer ,你可能會遇到的那些事

經過 npm init 初始化項目後, 就能夠安裝 Puppeteer 依賴了:

npm i puppeteer :在安裝時自動下載最新版本 Chromium。

或者

npm i puppeteer-core :在安裝時不會自動下載 Chromium。(不能生成截圖)

另外,在安裝過程當中可能會由於下載 Chromium 致使報錯,官網建議是先經過 npm i --save puppeteer --ignore-scripts 阻止下載 Chromium, 而後再手動下載 Chromium

手動下載後,須要配置指定路徑,修改 index.js 文件

const puppeteer = require('puppeteer');
(async () => {
      const browser = await puppeteer.launch({
        // 運行 Chromium 或 Chrome 可執行文件的路徑(相對路徑)
        executablePath: './chrome-mac/Chromium.app/Contents/MacOS/Chromium', 
        headless: false
      });
      const page = await browser.newPage();
      await page.goto('https://example.com');
      await page.screenshot({path: 'screenshot.png'});
      browser.close();
})();
複製代碼

快速建立測試用例

爲了提升測試腳本的可維護性、擴展性,咱們將測試用例的信息都配置到 JSON 文件中,這樣編寫測試腳本的時候,咱們只需關注測試流程的實現。

測試用例 JSON 數據配置包括公用數據(global)私有數據

公用數據(global):各測試用例都須要用到的數據,如:模擬訪問的目標頁面地址、名字、描述、設備類型等。

私有數據: 各測試用例特定的數據,如測試模塊信息、API 地址、測試場景、預期結果、截圖名字等數據。

{
  "global": {
    "url": "https://wqs.jd.com/xxx/index.shtml",
    "pageName": "index",
    "pageDesc": "首頁",
    "device": "iPhone 7"
  },
  "homePageApi": {
    "id": 1,
    "module": "home_page_api",
    "moduleDesc": "首頁主接口",
    "api": "https://wqcoss.jd.com/xxx",
    "operation": "模擬響應碼 500",
    "expectRules": [
      "1. 顯示異常信息、刷新按鈕",
      "2. 點擊刷新按鈕,顯示異常信息",
      "3. 恢復網絡,點擊刷新按鈕,顯示正常數據"
    ],
    "screenshot": [
      {
        "name": "normal",
        "desc": "正常場景"
      },
      {
        "name": "500_cache",
        "desc": "有緩存-返回500"
      },
      {
        "name": "500_no_cache",
        "desc": "無緩存-返回500"
      },
      {
        "name": "500_no_cache_reload",
        "desc": "無緩存-返回500-點擊刷新按鈕"
      },
      {
        "name": "500_no_cache_recover",
        "desc": "無緩存-返回500-恢復網絡"
      }
    ]
  },
  …
}
複製代碼

編寫測試腳本

咱們以京喜首頁主接口的測試用例爲例子,經過模接口返回 500 響應碼的異常場景,驗證主接口的異常處理機制是否完善、用戶體驗是否友好。

預期效果:

  • 有緩存狀況下,顯示緩存數據
  • 無緩存狀況下顯示異常信息、刷新按鈕
  • 點擊刷新按鈕,顯示異常信息
  • 恢復網絡,點擊刷新按鈕,顯示正常數據

測試流程:

方案流程圖

場景實現:

根據測試流程以及配置的測試用例信息,編寫測試腳本,實現測試用例場景:

  1. 訪問頁面
await page.goto(url)
複製代碼
  1. 生成截圖
await page.screenshot({
      path: './screenshot/index_home_page_500.png'
 })

複製代碼
  1. 攔截接口請求
async test () => {
  ... // 建立 Page 實例,訪問首頁
  await page.setRequestInterception(true) // 設置攔截請求
  page.on("request", interceptionEvent)   // 監聽請求事件,當請求發起後頁面會觸發這個事件
  ... // 刷新頁面,觸發請求攔截,生成測試場景截圖
}
複製代碼

若測試用例須要攔截不一樣的請求,或是模擬多種場景,則須要設置多個請求監聽事件。且一個事件執行結束後,必需要移除事件監聽,才能繼續下一個事件監聽。

添加事件監聽:page.on("request", eventFunction)

移除事件監聽:page.off("request", eventFunction)

// 設置攔截請求
    await page.setRequestInterception(true)
    const iconInterception1 = requestInterception(api, "body")
    // 添加事件 1 監聽
    page.on("request", iconInterception1)
    await page.goto(url)
    await page.screenshot({
      path: './screenshot/1.png'
    })
    // 移除事件 1 監聽 
    page.off("request", iconInterception1)
    const iconInterception2 = requestInterception(api, "body", )
    // 添加事件 2 監聽
    page.on("request", iconInterception2)
    await page.goto(url)
    await page.screenshot({
      path: './screenshot/2.png'
    })
    // 移除事件 2 監聽
    page.off("request", iconInterception2)
複製代碼
  1. 模擬異常數據場景,生成 mock 數據。
function requestInterception (api, setProps, setValue) {
  let mockData
  switch (setProps) {
    case "status":      // 修改返回狀態碼
      mockData = {
        status: setValue
      }
      break
    case "contentType": // 修改返回內容類型
      mockData = {
        contentType: setValue
      }
      break
    case "body":        // 修改返回數據
      mockData = {
        contentType: getMockResponse(setValue)
      }
      break
    default:
      break
  }
  return async req => {
   // 若是是須要攔截的 API,則經過 req.respond(mockData) 修改返回數據,不然 continue 繼續請求別的
    if (req.url().includes(api)) { // 攔截 API
      req.respond(mockData) // 修改返回數據
      return false  // 處理完了某個請求必須退出,再也不執行 continue
    }
    req.continue()
}
複製代碼

模擬接口返回 500:

const interception500 = requestInterception(api, 'status', 500)
  page.on("request", interception500) // 當請求發起後頁面會觸發這個事件
複製代碼

模擬異常數據:

const iconInterception = requestInterception(api, "body", { 
     "data": {
       "modules": [{
          "tpl": "3000",
          "content": []
        }]
      }
 })
 page.on("request", iconInterception)
 
複製代碼

生成 mock 數據有兩種實現方案,可依據實際狀況而定:

  • 直接經過修改接口真實返回的數據生成 mock 數據,須要先獲取接口實時返回數據
  • 本地存儲一份完整的接口數據,經過修改本地存儲數據的方式生成 mock 數據(本文所述案例均基於此方案實現)

若選擇第一種方案,則需先攔截接口請求,經過 req.response() 獲取接口實時返回數據,根據測試場景修改實時返回數據做爲 mock 數據。

因爲京喜 H5 頁面接口返回是 JSONP 格式的數據,因此在模擬返回數據的時候,必須先截取 JSONP 的 callback 信息,與模擬數據拼接後再返回;

function requestInterception (api, setProps, setValue) {
    let mockData
    switch (setProps) {
      case "status":
        mockData = {
          status: setValue
        }
        break
      case "contentType":
        mockData = {
          contentType: setValue
        }
        break
      default:
        break
    }
    return async req => {
      if (req.url().includes(api)) {
        if (setProps === "body") {
          const callback = getUrlParam("callback", req.url())  // 獲取 callback 信息
          const localData = getLocalMockResponse(api)  // 匹配 API ,獲取本地存儲數據
          mockData = {
            body: getResponseMockLocalData(localData, setValue, callback, api) // 生成 mock 數據
          }
        }
        req.respond(mockData)  // 設置返回數據
        return false
      }
      req.continue()
    }
  }
複製代碼
  1. 清除緩存
page.evaluate(() => {
    try {
      localStorage.clear()
      sessionStorage.clear()
    } catch (e) {
      console.log(e)
    }
})
複製代碼
  1. 點擊刷新按鈕
await page.waitFor(".page-error__refresh-btn") // 能夠傳 CSS 選擇器,也能夠傳時間(單位毫秒)
await page.click(".page-error__refresh-btn")
複製代碼

在模擬點擊刷新按鈕以前,需等待按鈕渲染完成,再觸發按鈕點擊。(防止刷新頁面後,DOM 還未渲染完成的狀況下,因找不到 DOM 致使報錯)

  1. 取消攔截,恢復網絡
await page.setRequestInterception(false)

複製代碼

運行腳本及調試

因爲第一階段的測試工具還沒有平臺化,自動化測試流程先經過在終端輸入命令行,運行腳本的方式啓動。

在項目的 package.json 文件中,使用 scripts 字段定義腳本命令:

"scripts": {
    "test:real": "node ./pages/index/index.js",
    "test:mock": "node ./pages/index-mock/index.js"
  },
複製代碼

運行:

在終端切入到項目根目錄路徑,輸入如下命令行,就能夠啓動測試工具,運行測試腳本。

- npm run test:real                     // 接口真實返回的數據測試
- npm run test:mock                     // 使用本地 mock 數據測試
複製代碼

調試:

開啓調試模式以前,須要先了解 Headless Chrome

Headless Chrome ,無頭模式,瀏覽器的無界面形態,能夠在不打開瀏覽器的前提下,在命令行中運行測試腳本,可以徹底像真實瀏覽器同樣完成用戶全部操做,不用擔憂運行測試腳本時瀏覽器受到外界的干擾,也不須要藉助任何顯示設備,使自動化測試更穩定。

Puppeteer 默認以無頭模式運行。

那麼要開啓調試模式,就必須取消無頭模式,在打開瀏覽器的場景下,進行自動化測試。所以,在命令行腳本中增長了「取消無頭模式」和「打開開發者工具」的參數,測試腳本經過獲取到的參數,決定是否開啓調試模式。

const headless = process.argv[2] !== 'head'  // 獲取是否開啓無頭模式參數
const devtools = process.argv[3] === 'dev'   // 獲取是否打開開發者工具參數
const browser = await puppeteer.launch({
      executablePath: browserPath,
      headless,
      devtools
    })
複製代碼

在終端切入到項目根目錄路徑,輸入如下命令行,就能夠開啓調試模式,運行測試腳本。

- npm run test:mock head            // 打開 Chromium 窗口
- npm run test:mock head dev        // 打開 Chromium 窗口 和 開發者工具窗口
複製代碼
  • head 參數:取消無頭模式,打開 Chromium 窗口運行腳本;
  • head dev 參數:在打開 Chromium 窗口運行腳本,並打開 Devtools 窗口,開啓調試模式。

測試結果

人工比對截圖結果:

測試結果圖

運行腳本示例:

方案流程圖

更多測試場景實現

1. 截取從頁面頂部到指定 DOM 之間的區域(內容可能超出一屏的長圖)

Puppeteer 提供了四種截圖方式:

(1)截取一屏內容(默認普通截屏);
(2)截取指定 DOM;
(3)截取全屏;
(4)指定裁剪區域,可設置 x、y、width、height。 x, y 是相對頁面左上角。但只能截取一屏的內容,超出一屏不展現。
複製代碼

基於第四種方法進行改造:

  1. 經過原生 JavaScript 的 getBoundingClientRect() 方法獲取到指定 DOM 的 x,y 座標值;
  2. 經過 page.setViewport() 重置視口的高度;
  3. 調用截圖 API 生成截圖。
async function screenshotToElement (page, selector, path) {
    try {
      await page.waitForSelector(selector)
      let clip = await page.evaluate(selector => {
        const element = document.querySelector(selector)
        let { x, y, width, height } = element.getBoundingClientRect()
        return {
          x: 0,
          y: 0,
          width,
          height: M(y),  
        }
      }, selector)
      await page.setViewport(clip)
      await page.screenshot({
        path: path,
        clip: clip
      })
    } catch (e) {
      console.log(e)
    }
  }
複製代碼
  • height: y:截到指定 DOM 的頂部,不包含該 DOM;
  • height: y + height: 截到指定 DOM 的底部,包含該 DOM;
  • 原生 Javascript 的 getBoundingClientRect() 方法獲取 DOM 元素定位和寬高值多是小數,而 Puppeteer 的 setViewport() 設置視口方法不支持小數,因此須要對獲取到的 DOM 元素定位信息取整。

2. 模擬不一樣渠道,如:手Q場景:

// 設置 UA 
await page.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 10_2_1 like Mac OS X) AppleWebKit/602.4.6 (KHTML, like Gecko) Mobile/14D27 QQ/6.7.1.416 V1_IPH_SQ_6.7.1_1_APP_A Pixel/750 Core/UIWebView NetType/4G QBWebViewType/1")
複製代碼

3. 滾動頁面

await page.evaluate((top) => {
    window.scrollTo(0, top)
 }, top)
複製代碼

page.evaluate(pageFunction, …args):在當前頁面實例上下文中執行 JavaScript 代碼

4. 監聽頁面崩潰事件

// 當頁面崩潰時觸發
page.on('error', (e) => {
    console.log(e)
})
複製代碼

結語

第一階段的 H5 自動化之路告一段落,容災演習已實現了半自動化,可經過在終端運行測試腳本,模擬異常場景自動生成截圖,再配合人工比對截圖操做,判斷演習結果是否符合預期。目前已投入到每月的容災演習中使用。

隨着京喜業務的迭代,頁面也將更新改版,所以測用例也須要持續維護和更新。後續將持續優化自動化工具,共享測試腳本、在生成截圖的基礎上自動比對測試結果是否符合預期、數據入庫、將測試結果轉化成文檔,自動發送郵件等等。基於容災演習的自動化測試,還可擴展廣告位的監測,數據上報監自動化測試……

對於京喜首頁自動化測試之路,遠沒有結束,還有不少能夠優化和擴展的地方,接下來分階段持續優化自動化測試工具,敬請期待!

相關連接

Puppeteer


歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章:

image
相關文章
相關標籤/搜索