齊磊
ThoughtWorks 資深質(zhì)量保證咨詢師,超過 7 年的軟件開發(fā)與測試經(jīng)驗,擅長測試開發(fā),軟件開發(fā)流程的自動化,目前致力于開闊移動端測試開發(fā)領域。
自 2017 年中以來,Chrome 用戶可以選擇以 headless 模式運行瀏覽器。此功能非常適合運行前端瀏覽器測試,而無需在屏幕上顯示操作過程。在此之前,這主要是 PhantomJS 的領地,但 Headless Chrome 正在迅速取代這個由 JavaScript 驅(qū)動的 WebKit 方法。Headless Chrome 瀏覽器的測試運行速度要快得多,而且行為上更像一個真正的瀏覽器,雖然我們的團隊發(fā)現(xiàn)它比 PhantomJS 使用更多的內(nèi)存。有了這些優(yōu)勢,用于前端測試的 Headless Chrome 很可能成為事實上的標準。
隨著 Google 在 Chrome 59 版本放出了 headless 模式,Ariya Hidayat 決定放棄對 Phantom.js 的維護,這也標示著 Phantom.js 統(tǒng)治 fully functional headless browser 的時代將被 chrome-headless 代替。
Headless Browser
也許很多人對無頭瀏覽器還是很陌生,我們先來看看維基百科的解釋:
A headless browser is a web browser without a graphical user interface.
Headless browsers provide automated control of a web page in an environment similar to popular web browsers, but are executed via a command-line interface or using network communication.
對,就是沒有頁面的瀏覽器。多用于測試 web、截圖、圖像對比、測試前端代碼、爬蟲(雖然很慢)、監(jiān)控網(wǎng)站性能等。
為什么要使用 headless 測試?
headless broswer 可以給測試帶來顯著好處:
- 對于 UI 自動化測試,少了真實瀏覽器加載 css,js 以及渲染頁面的工作。無頭測試要比真實瀏覽器快的多。
- 可以在無界面的服務器或 CI 上運行測試,減少了外界的干擾,使自動化測試更穩(wěn)定。
- 在一臺機器上可以模擬運行多個無頭瀏覽器,方便進行并發(fā)測試。
headless browser 有什么缺陷?
以 phantomjs 為例
- 雖然 Phantom.js 是 fully functional headless browser,但是它和真正的瀏覽器還是有很大的差別,并不能完全模擬真實的用戶操作。很多時候,我們在 Phantom.js 發(fā)現(xiàn)一些問題,但是調(diào)試了半天發(fā)現(xiàn)是 Phantom.js 自己的問題。
- 將近 2k 的 issue,仍然需要人去修復。
- Javascript 天生單線程的弱點,需要用異步方式來模擬多線程,隨之而來的 callback 地獄,對于新手而言非常痛苦,不過隨著 es6 的廣泛應用,我們可以用 promise 來解決多重嵌套回調(diào)函數(shù)的問題。
- 雖然 webdriver 支持 htmlunit 與 phantomjs,但由于沒有任何界面,當我們需要進行調(diào)試或復現(xiàn)問題時,就非常麻煩。
那么 Headless Chrome 與上面提到 fully functional headless browser 又有什么不同呢?
什么是 Headless Chrome?
Headless Chrome 是 Chrome 瀏覽器的無界面形態(tài),可以在不打開瀏覽器的前提下,使用所有 Chrome 支持的特性,在命令行中運行你的腳本。相比于其他瀏覽器,Headless Chrome 能夠更加便捷的運行 web 自動化測試、編寫爬蟲、截取圖等功能。
有的人肯定會問:看起來它的作用和 phantomjs 沒什么具體的差別?
對,是的,Headless Chrome 發(fā)布就是來代替 phantomjs。
我們憑什么換用 Headless Chrome?
- 我爸是 Google,那么就意味不會出現(xiàn) phantomjs 近 2k 問題沒人維護的尷尬局面。 比 phantomjs 有更快更好的性能。
- 有人已經(jīng)做過實驗,同一任務,Headless Chrome 要比現(xiàn) phantomjs 更加快速的完成任務,且占用內(nèi)存更少。(https:///benchmark-headless-chrome-vs-phantomjs-e7f44c6956c)
- chrome 對 ECMAScript 2017 (ES8) 支持,同樣 headless 隨著 chrome 更新,意味著我們也可以使用最新的 js 語法來編寫的腳本,例如 async,await 等。
- 完全真實的瀏覽器操作,chrome headless 支持所有 chrome 特性。
- 更加便利的調(diào)試,我們只需要在命令行中加入–remote-debugging-port=9222,再打開瀏覽器輸入 localhost:9222(ip 為實際運行命令的 ip 地址)就能進入調(diào)試界面。
能帶給 QA 以及項目什么好處?
前端測試改進
以目前的項目來說,之前的前端單元測試以及組件測試是用 karma 在 phantomjs 運行的,非常不穩(wěn)定,在遠端 CI 上運行時經(jīng)常會莫名其妙的掛掉,也找不出來具體的原因,自從 Headless Chrome 推出后,我們將 phantomjs 切換成 Headless Chrome,再也沒有出現(xiàn)過異常情況,切換也非常簡單,只需要把 karma.conf.js 文件中的配置改下就 OK 了。如下
customLaunchers: { myChrome: { base: 'ChromeHeadless', flags: ['--no-sandbox', '--disable-gpu', '--remote-debugging-port=9222'] } },
browsers: ['myChrome'],
UI 功能測試改進
原因一,Chrome-headless 能夠完全像真實瀏覽器一樣完成用戶所有操作,再也不用擔心跑測試時,瀏覽器受到干擾,造成測試失敗
原因二,之前如果我們像要在 CI 上運行 UI 自動化測試,非常麻煩。必須使用 Xvfb 幫助才能在無界面的 Linux 上運行 UI 自動化測試。(Xvfb 是一個實現(xiàn)了 X11 顯示服務協(xié)議的顯示服務器。 不同于其他顯示服務器,Xvfb 在內(nèi)存中執(zhí)行所有的圖形操作,不需要借助任何顯示設備。)現(xiàn)在也只需要在 webdriver 啟動時,設置一下 chrome option 即可,以 capybara 為例:
Capybara.register_driver :selenium_chrome do app
Capybara::Selenium::Driver.new (app, browser: :chrome,
desired_capabilities: {
"chromeOptions" => {
"args" => [ "--incognito",
"--allow-running-insecure-content",
"--headless",
"--disable-gpu"
]}
})
end
無縫切換,只需更改下配置,就可以提高運行速度與穩(wěn)定性,何樂而不為。
Google 終極大招
Google 最近放出了終極大招——Puppeteer(Puppeteer is a Node library which provides a high-level API to control headless Chrome over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome.)
類似于 webdriver 的高級別的 api,去幫助我們通過 DevTools 協(xié)議控制無界面 Chrome。
在 puppteteer 之前,我們要控制 chrome headless 需要使用 chrome-remote-interface 來實現(xiàn),但是它比 Puppeteer API 更接近低層次實現(xiàn),無論是閱讀還是編寫都要比 puppteteer 更復雜。也沒有具體的 dom 操作,尤其是我們要模擬一下 click 事件,input 事件等,就顯得力不從心了。
我們用同樣 2 段代碼來對比一下 2 個庫的區(qū)別。
首先來看看 chrome-remote-interface
const chromeLauncher = require ('chrome-launcher');
const CDP = require ('chrome-remote-interface');
const fs = require ('fs');
function launchChrome (headless=true) {
return chromeLauncher.launch ({
// port: 9222, // Uncomment to force a specific port of your choice.
chromeFlags: [
'--window-size=412,732',
'--disable-gpu',
headless ? '--headless' : ''
]
});
}
(async function () {
const chrome = await launchChrome ();
const protocol = await CDP ({port: chrome.port});
const {Page, Runtime} = protocol;
await Promise.all ([Page.enable (), Runtime.enable ()]);
Page.navigate ({url: 'https://www.github.com/'});
await Page.loadEventFired (
console.log ("start")
);
const {data} = await Page.captureScreenshot ();
fs.writeFileSync ('example.png', Buffer.from (data, 'base64'));
// Wait for window.onload before doing stuff.
protocol.close ();
chrome.kill (); // Kill Chrome.
再來看看 puppeteer
const puppeteer = require ('puppeteer');
(async () => {
const browser = await puppeteer.launch ();
const page = await browser.newPage ();
await page.goto ('https://www.github.com');
await page.screenshot ({path: 'example.png'});
await browser.close ();
})();
對,就是這么簡短明了,更接近自然語言。沒有 callback,幾行代碼就能搞定我們所需的一切。
總結(jié)
目前 Headless Chrome 仍然存在一些問題,還需要不斷完善,我們應該擁抱變化,適應它,讓它給我們的工作帶來更多幫助。