測試你的前端代碼 - part3(端到端測試)

本文做者: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 服務器的設置。

設置 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 成爲異步函數。

你是否有好奇 prepareDrivercleanupDriver 函數長什麼樣呢?一塊兒來看下:

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 感興趣的童鞋,歡迎指點

相關文章
相關標籤/搜索