摘要:單元測試幾乎不會出現不穩定的情況,因為單元測試通常是簡單輸入,簡單輸出。鏈接直達測試你的前端代碼集成測試。
本文作者:Gil Tayar
編譯:胡子大哈翻譯原文:http://huziketang.com/blog/posts/detail?postId=58d50da37413fc2e8240855c
英文連接:Testing Your Frontend Code: Part III (E2E Testing)
轉載請注明出處,保留原文鏈接以及作者信息
上一篇文章《測試你的前端代碼 - part2(單元測試)》中,我介紹了關于單元測試的基本知識,從本文介紹端到端測試(E2E 測試)。
端到端測試在第二部分中,我們使用 Mocha 測試了應用中最核心的邏輯,calculator 模塊。本文中我們將使用端到端測試整個應用,實際上是模擬了用戶所有可能的操作進行測試。
在我們的例子中,計算器展示出來的前端即為整個應用,因為沒有后端。所以端到端測試就是說直接在瀏覽器中運行應用,通過鍵盤做一系列計算操作,且保證所展示的輸出結果都是正確的。
是否需要像單元測試那樣,測試各種組合呢?并不是,我們已經在單元測試中測試過了,端到端測試不是檢查某個單元是否 ok,而是把它們放到一起,檢查還是否能夠正確運行。
需要多少端到端測試首先給出結論:端到端測試不需要太多。
第一個原因,如果已經通過了單元測試和集成測試,那么可能已經把所有的模塊都測試過了。那么端到端測試的作用就是把所有的單元測試綁到一起進行測試,所以不需要很多端到端測試。
第二個原因,這類測試一般都很慢。如果像單元測試那樣有幾百個端到端測試,那運行測試將會非常慢,這就違背了一個很重要的測試原則——測試迅速反饋結果。
第三個原因,端到端測試的結果有時候會出現 flaky 的情況。Flaky 測試是指通常情況下可以測試通過,但是偶爾會出現測試失敗的情況,也就是不穩定測試。單元測試幾乎不會出現不穩定的情況,因為單元測試通常是簡單輸入,簡單輸出。一旦測試涉及到了 I/O,那么不穩定測試可能就出現了。那可以減少不穩定測試嗎?答案是肯定的,可以把不穩定測試出現的頻率減少到可以接受的程度。那能夠徹底消除不穩定測試嗎?也許可以,但是我到現在還沒見到過[笑著哭]。
所以為了減少我們測試中的不穩定因素,盡量減少端到端測試。10 個以內的端到端測試,每個都測試應用的主要工作流。
寫端到端測試代碼好了,廢話不多說,開始介紹寫端到端代碼。首先需要準備好兩件事情:1. 一個瀏覽器;2. 運行前端代碼的服務器。
因為要使用 Mocha 進行端到端測試,就和之前單元測試一樣,需要先對瀏覽器和 web 服務器進行一些配置。使用 Mocha 的 before 鉤子設置初始化狀態,使用 after 鉤子清理測試后狀態。before 和 after 鉤子分別在測試的開始和結束時運行。
下面一起來看下 web 服務器的設置。
設置 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 也會經常使用,這就是為什么在端到端測試中需要重試的原因。
測試 Element來看測試的下一階段,測試元素:
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現在該來從 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 來運行所有測試:
一次性全部通過!(這是當然的了,不然怎么寫文章。)
想說點關于使用 await 的一些話你在可能網絡上其他地方看到一些例子,它們并沒有使用 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 感興趣的童鞋,歡迎指點。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82323.html
摘要:單元測試幾乎不會出現不穩定的情況,因為單元測試通常是簡單輸入,簡單輸出。鏈接直達測試你的前端代碼集成測試。 本文作者:Gil Tayar 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58d50da37413fc2e8240855c 英文連接:Testing Your Frontend Code: Part ...
摘要:測試光譜光譜的一端單元測試顧名思義,代碼以單元為單位進行測試。這個系列文章整體如下測試你的前端代碼單元測試測試你的前端代碼端到端測試測試你的前端代碼集成測試。 showImg(https://segmentfault.com/img/remote/1460000008812278?w=998&h=354); 本文作者:Gil Tayar 編譯:胡子大哈 翻譯原文:http://hu...
摘要:測試光譜光譜的一端單元測試顧名思義,代碼以單元為單位進行測試。這個系列文章整體如下測試你的前端代碼單元測試測試你的前端代碼端到端測試測試你的前端代碼集成測試。 showImg(https://segmentfault.com/img/remote/1460000008812278?w=998&h=354); 本文作者:Gil Tayar 編譯:胡子大哈 翻譯原文:http://hu...
摘要:單元測試上一節有討論過,單元測試就是以代碼單元為單位進行測試,代碼單元可以是一個函數,一個模塊,或者一個類。單元測試是最容易理解也最容易實現的測試方式。在寫單元測試的時候,盡量將你的單元測試獨立出來,不要幾個單元互相引用。 showImg(https://segmentfault.com/img/remote/1460000008823416?w=997&h=350); 本文作者:G...
摘要:單元測試上一節有討論過,單元測試就是以代碼單元為單位進行測試,代碼單元可以是一個函數,一個模塊,或者一個類。單元測試是最容易理解也最容易實現的測試方式。在寫單元測試的時候,盡量將你的單元測試獨立出來,不要幾個單元互相引用。 showImg(https://segmentfault.com/img/remote/1460000008823416?w=997&h=350); 本文作者:G...
閱讀 2799·2021-11-22 14:44
閱讀 539·2021-11-22 12:00
閱讀 3681·2019-08-30 15:54
閱讀 1568·2019-08-29 17:15
閱讀 1896·2019-08-29 13:50
閱讀 1106·2019-08-29 13:17
閱讀 3511·2019-08-29 13:05
閱讀 1180·2019-08-29 11:31