本文做者:Gil Tayar
編譯:鬍子大哈 javascript翻譯原文:http://huziketang.com/blog/posts/detail?postId=58d50da37413fc2e8240855c
英文鏈接:Testing Your Frontend Code: Part III (E2E Testing)css
轉載請註明出處,保留原文連接以及做者信息html
上一篇文章《測試你的前端代碼 - part2(單元測試)》中,我介紹了關於單元測試的基本知識,從本文介紹端到端測試(E2E 測試)。前端
在第二部分中,咱們使用 Mocha 測試了應用中最核心的邏輯,calculator 模塊。本文中咱們將使用端到端測試整個應用,其實是模擬了用戶全部可能的操做進行測試。java
在咱們的例子中,計算器展現出來的前端即爲整個應用,由於沒有後端。因此端到端測試就是說直接在瀏覽器中運行應用,經過鍵盤作一系列計算操做,且保證所展現的輸出結果都是正確的。node
是否須要像單元測試那樣,測試各類組合呢?並非,咱們已經在單元測試中測試過了,端到端測試不是檢查某個單元是否 ok,而是把它們放到一塊兒,檢查仍是否可以正確運行。react
首先給出結論:端到端測試不須要太多。webpack
第一個緣由,若是已經經過了單元測試和集成測試,那麼可能已經把全部的模塊都測試過了。那麼端到端測試的做用就是把全部的單元測試綁到一塊兒進行測試,因此不須要不少端到端測試。git
第二個緣由,這類測試通常都很慢。若是像單元測試那樣有幾百個端到端測試,那運行測試將會很是慢,這就違背了一個很重要的測試原則——測試迅速反饋結果。github
第三個緣由,端到端測試的結果有時候會出現 flaky 的狀況。Flaky 測試是指一般狀況下能夠測試經過,可是偶爾會出現測試失敗的狀況,也就是不穩定測試。單元測試幾乎不會出現不穩定的狀況,由於單元測試一般是簡單輸入,簡單輸出。一旦測試涉及到了 I/O,那麼不穩定測試可能就出現了。那能夠減小不穩定測試嗎?答案是確定的,能夠把不穩定測試出現的頻率減小到能夠接受的程度。那可以完全消除不穩定測試嗎?也許能夠,可是我到如今還沒見到過[笑着哭]。
因此爲了減小咱們測試中的不穩定因素,儘可能減小端到端測試。10 個之內的端到端測試,每一個都測試應用的主要工做流。
好了,廢話很少說,開始介紹寫端到端代碼。首先須要準備好兩件事情:1. 一個瀏覽器;2. 運行前端代碼的服務器。
由於要使用 Mocha 進行端到端測試,就和以前單元測試同樣,須要先對瀏覽器和 web 服務器進行一些配置。使用 Mocha 的 before 鉤子設置初始化狀態,使用 after 鉤子清理測試後狀態。before 和 after 鉤子分別在測試的開始和結束時運行。
下面一塊兒來看下 web 服務器的設置。
配置一個 Node Web 服務器,首先想到的就是 express 了,話很少說,直接上代碼:
let server before((done) => { const app = express() app.use('/', express.static(path.resolve(__dirname, '../../dist'))) server = app.listen(8080, done) }) after(() => { server.close() })
代碼中,before
鉤子中建立一個 express 應用,指向 dist
文件夾,而且監聽 8080 端口,結束的時候在 after
鉤子中關閉服務器。
dist
文件夾是什麼?是咱們打包 JS 文件的地方(使用 Webpack打包),HTML 文件,CSS 文件也都在這裏。能夠看一下 package.json
的代碼:
{ "name": "frontend-testing", "scripts": { "build": "webpack && cp public/* dist", "test": "mocha 'test/**/test-*.js' && eslint test lib", ... },
對於端到端測試,要記得在執行 npm test
以前,先執行 npm run build
。其實這樣很不方便,想一下以前的單元測試,不須要作這麼複雜的操做,就是由於它能夠直接在 node 環境下運行,既不用轉譯,也不用打包。
出於完整性考慮,看一下 webpack.config.js
文件,它是用來告訴 webpack 怎樣處理打包:
module.exports = { entry: './lib/app.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, ... }
上面的代碼指的是,Webpack 會讀取 app.js
文件,而後將 dist
文件夾中全部用到的文件都打包到 bundle.js
中。dist
文件夾會同時應用在生產環境和端到端測試環境。這裏要注意一個很重要的事情,端到端測試的運行環境要儘可能和生產環境保持一致。
如今咱們已經設置完了後端,應用已經有了服務器提供服務了,如今要在瀏覽器中運行咱們的計算器應用。用什麼包來驅動自動執行程序呢,我常用 selenium-webdriver
,這是一個很流行的包。
首先看一下如何使用驅動:
const {prepareDriver, cleanupDriver} = require('../utils/browser-automation') //... describe('calculator app', function () { let driver ... before(async () => { driver = await prepareDriver() }) after(() => cleanupDriver(driver)) it('should work', async function () { await driver.get('http://localhost:8080') //... }) })
before
中,準備好驅動,在 after
中把它清理掉。準備好驅動後,會自動運行瀏覽器(Chrome,稍後會看到),清理掉之後會關閉瀏覽器。這裏注意,準備驅動的過程是異步的,返回一個 promise,因此咱們使用 async/await 功能來使代碼看起來更美觀(Node7.7,第一個本地支持 async/await 的版本)。
最後在測試函數中,傳遞網址:http:/localhost:8080
,仍是使用 await,讓 driver.get
成爲異步函數。
你是否有好奇 prepareDriver
和 cleanupDriver
函數長什麼樣呢?一塊兒來看下:
const webdriver = require('selenium-webdriver') const chromeDriver = require('chromedriver') const path = require('path') const chromeDriverPathAddition = `:${path.dirname(chromeDriver.path)}` exports.prepareDriver = async () => { process.on('beforeExit', () => this.browser && this.browser.quit()) process.env.PATH += chromeDriverPathAddition return await new webdriver.Builder() .disableEnvironmentOverrides() .forBrowser('chrome') .setLoggingPrefs({browser: 'ALL', driver: 'ALL'}) .build() } exports.cleanupDriver = async (driver) => { if (driver) { driver.quit() } process.env.PATH = process.env.PATH.replace(chromeDriverPathAddition, '') }
能夠看到,上面這段代碼很笨重,並且只能在 Unix 系統上運行。理論上,你能夠不用看懂,直接複製/粘貼到你的測試代碼中就能夠了,這裏我仍是深刻講一下。
前兩行引入了 webdriver 和咱們使用的瀏覽器驅動 chromedriver。Selenium Webdriver 的工做原理是經過 API(第一行中引入的 selenium-webdriver
)調用瀏覽器,這依賴於被調瀏覽器的驅動。本例中被調瀏覽器驅動是 chromedriver
,在第二行引入。
chrome driver 不須要在機器上裝了 Chrome,實際上在你運行 npm install
的時候,已經裝了它自帶的可執行 Chrome 程序。接下來 chromedriver
的目錄名須要添加進環境變量中,見代碼中的第 9 行,在清理的時候再把它刪掉,見代碼中第 22 行。
設置了瀏覽器驅動之後,咱們來設置 web driver,見代碼的 11 - 15 行。由於 build
函數是異步的,因此它也使用 await
。到如今爲止,驅動部分就已經設置完畢了。
設置完驅動之後,該看一下測試的代碼了。完整的測試代碼在這裏,下面列出部分代碼:
// ... const retry = require('promise-retry') // ... it('should work', async function () { await driver.get('http://localhost:8080') await retry(async () => { const title = await driver.getTitle() expect(title).to.equal('Calculator') }) //...
這裏的代碼調用計算器應用,檢查應用標題是否是 「Calculator」。代碼中第 6 行,給瀏覽器賦地址:http://localhost:8080
,記得要使用 await
。再看第 9 行,調用瀏覽器而且返回瀏覽器的標題,在第 10 行中與預期的標題進行比較。
這裏還有一個問題,這裏引入了 promise-retry
模塊進行重試,爲何須要重試?緣由是這樣的,當咱們告訴瀏覽器執行某命令,好比定位到一個 URL,瀏覽器會去執行,可是是異步執行。瀏覽器執行的很是快,這時候對於開發人員來說,確切地知道瀏覽器「正在執行」,要比僅僅知道一個結果更重要。正是由於瀏覽器執行的很是快,因此若是不重試的話,很容易被 await
所愚弄。在後面的測試中 promise-retry
也會常用,這就是爲何在端到端測試中須要重試的緣由。
來看測試的下一階段,測試元素:
const {By} = require('selenium-webdriver') it('should work', async function () { await driver.get('http://localhost:8080') //... await retry(async () => { const displayElement = await driver.findElement(By.css('.display')) const displayText = await displayElement.getText() expect(displayText).to.equal('0') }) //...
下一個要測試的是初始化狀態下所顯示的是否是 「0」,那麼首先就須要找到控制顯示的 element,在咱們的例子中是 display
。見第 7 行代碼,webdriver 的 findElement
方法返回咱們所要找的元素。能夠經過 By.id
或者 By.css
再或者其餘找元素的方法。這裏我使用 By.css
,它很經常使用,另外提一句 By.javascript
也很經常使用。
(不知道你是否注意到,By
是由最上面的 selenium-webdriver
所引入的)
當咱們獲取到了 element 之後,就可使用 getText()
(還可使用其餘操做 element 的函數),來獲取元素文本,而且檢查它是否和預期同樣,見第 10 行。對了,不要忘記:
如今該來從 UI 層面測試應用了,點擊數字和操做符,測試計算器是否是按照預期的運行:
const digit4Element = await driver.findElement(By.css('.digit-4')) const digit2Element = await driver.findElement(By.css('.digit-2')) const operatorMultiply = await driver.findElement(By.css('.operator-multiply')) const operatorEquals = await driver.findElement(By.css('.operator-equals')) await digit4Element.click() await digit2Element.click() await operatorMultiply.click() await digit2Element.click() await operatorEquals.click() await retry(async () => { const displayElement = await driver.findElement(By.css('.display')) const displayText = await displayElement.getText() expect(displayText).to.equal('84') })
代碼 2 - 4 行,定義數字和操做;6 - 10 行模擬點擊。實際上想實現的是 「42 * 2 = 」。最終得到正確的結果——「84」。
已經介紹完了端到端測試和單元測試,如今用 npm test
來運行全部測試:
一次性所有經過!(這是固然的了,否則怎麼寫文章。)
你在可能網絡上其餘地方看到一些例子,它們並無使用 async/await
,或者是使用了 promise
。實際上這樣的代碼是同步的。那麼爲何也能 work 的很好呢?坦白地說,我也不知道,看起來像是在 webdriver 中有些 trick 的處理。正如 selenium 文檔中說道,在 Node 支持 async/await
以前,這是一個臨時的解決方案。
Selenium 文檔是 Java 語言。它還不完整,可是包含的信息也足夠了,你作幾回測試就能掌握這個技能。
本文中主要介紹了什麼:
介紹了端到端測試中設置瀏覽器的代碼;
介紹瞭如何使用 webdriver API 來調用瀏覽器,以及如何獲取 DOM 中的 element;
介紹了使用 async/await,由於全部 webdriver API 都是異步的;
介紹了爲何端到端測試中要使用 retry。
介紹完了端到端測試,下文該介紹「測試光譜」的中間部分了,即集成測試。連接直達:《測試你的前端代碼 - part4(集成測試)》。
我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點。