React 組件如何讓 UI 測試變得簡單
已發表: 2022-03-11測試後端很容易。 你選擇你喜歡的語言,將它與你最喜歡的框架配對,編寫一些測試,然後點擊“運行”。 你的控制台說“耶! 有用!” 您的持續集成服務會在每次推送時運行您的測試,生活很美好。
當然,測試驅動開發 (TDD) 起初很奇怪,但是可預測的環境、多個測試運行器、嵌入框架的測試工具以及持續集成支持,讓生活變得輕鬆。 五年前,我認為測試可以解決我遇到的所有問題。
然後Backbone變大了。
我們都切換到前端 MVC。 我們的可測試後端變成了美化的數據庫服務器。 我們最複雜的代碼移到了瀏覽器中。 而且我們的應用程序在實踐中不再可測試。
那是因為測試前端代碼和 UI 組件有點困難。
如果我們只想檢查我們的模型是否表現良好,那還不錯。 或者,調用函數將更改正確的值。 對於 React 單元測試,我們需要做的就是:
- 編寫格式良好、獨立的模塊。
- 使用 Jasmine 或 Mocha 測試(或其他)來運行函數。
- 使用測試運行器,例如 Karma 或 Chutzpah。
而已。 我們的代碼經過單元測試。
過去,運行前端測試是最困難的部分。 每個框架都有自己的想法,在大多數情況下,您最終會得到一個瀏覽器窗口,每次您想要運行測試時都需要手動刷新它。 當然,你總是會忘記。 至少,我知道我做到了。
2012 年,Vojta Jina 發布了 Karma runner(當時稱為 Testacular)。 借助 Karma,前端測試成為工具鏈的完整公民。 我們的 React 測試在終端或持續集成服務器上運行,當我們更改文件時它們會重新運行,我們甚至可以同時在多個瀏覽器中測試我們的代碼。
我們還能期望什麼? 好吧,來實際測試我們的前端代碼。
前端測試需要的不僅僅是單元測試
單元測試很棒:它是查看算法是否每次都做正確的事情,或檢查我們的輸入驗證邏輯、數據轉換或任何其他孤立操作的最佳方法。 單元測試非常適合基礎。
但是前端代碼不是關於操縱數據的。 它是關於用戶事件和在正確的時間呈現正確的視圖。 前端是關於用戶的。
這是我們希望能夠做到的:
- 測試 React 用戶事件
- 測試對這些事件的響應
- 確保在正確的時間呈現正確的內容
- 在許多瀏覽器中運行測試
- 對文件更改重新運行測試
- 使用 Travis 等持續集成系統
在我一直這樣做的十年中,直到我開始研究 React 之前,我還沒有找到一種體面的方法來測試用戶交互和視圖渲染。
React 單元測試:UI 組件
React 是實現這些目標的最簡單方法。 部分是因為它迫使我們使用可測試的模式來構建應用程序,部分是因為有很棒的 React 測試工具。
如果你以前從未使用過 React,你應該看看我的書React+d3.js 。 它面向可視化,但有人告訴我它是 React 的“一個很棒的輕量級介紹” 。
React 迫使我們將所有東西都構建為“組件”。 您可以將 React 組件視為小部件,或具有某些邏輯的 HTML 塊。 它們遵循函數式編程的許多最佳原則,但它們是對象。
例如,給定相同的參數集,React 組件將始終呈現相同的輸出。 不管它被渲染了多少次,不管是誰渲染它,不管我們把輸出放在哪裡。 總是一樣。 因此,我們不必執行複雜的腳手架來測試 React 組件。 他們只關心他們的屬性,不需要跟踪全局變量和配置對象。
我們在很大程度上通過避免狀態來實現這一點。 您可以在函數式編程中將此稱為引用透明性。 我不認為 React 中有專門的名稱,但官方文檔建議盡可能避免使用 state。
在測試用戶交互時,React 為我們提供了綁定到函數回調的事件。 設置測試間諜並確保點擊事件調用正確的函數很容易。 而且因為 React 組件會呈現自己,所以我們可以觸發一個點擊事件並檢查 HTML 是否有變化。 這是有效的,因為 React 組件只關心它自己。 點擊這裡不會改變那裡的事情。 我們將永遠不必處理嵌套的事件處理程序,只需定義良好的函數調用。
哦,因為 React 很神奇,我們不必擔心 DOM。 React 使用所謂的虛擬 DOM 將組件渲染為 JavaScript 變量。 對虛擬 DOM 的引用是我們測試 React 組件所需要的,真的。
它很甜。
React 的TestUtils
React 帶有一套內置的TestUtils
。 甚至還有一個推薦的名為 Jest 的測試運行程序,但我不喜歡它。 我稍後會解釋為什麼。 首先, TestUtils
。
我們通過類似require('react/addons').addons.TestUtils
獲得它們。 這是我們測試用戶交互和檢查輸出的入口點。
React TestUtils
讓我們通過將它的 DOM 放在一個變量中來渲染一個 React 組件,而不是將它插入到頁面中。 例如,要渲染一個 React 組件,我們會這樣做:
var component = TestUtils.renderIntoDocument( <MyComponent /> );
然後我們可以使用TestUtils
來檢查是否所有的孩子都被渲染了。 像這樣的東西:
var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );
findRenderedDOMComponentWithTag
會做它聽起來的樣子:遍歷孩子,找到我們正在尋找的組件,然後返回它。 返回的值將表現得像一個 React 組件。
然後我們可以使用getDOMNode()
來訪問原始 DOM 元素並測試它的值。 要檢查組件中的h1
標籤是否顯示“A title” ,我們會這樣寫:
expect(h1.getDOMNode().textContent) .toEqual("A title");
放在一起,完整的測試看起來像這樣:
it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });
很酷的部分是 TestUtils 還允許我們觸髮用戶事件。 對於點擊事件,我們會這樣寫:
var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);
這會模擬單擊並觸發任何潛在的偵聽器,這些偵聽器應該是更改輸出、狀態或兩者的組件方法。 如有必要,這些偵聽器可以調用父組件上的函數。
所有情況都易於測試:更改後的狀態在component.state
中,我們可以使用普通的 DOM 函數訪問輸出,並使用間諜進行函數調用。
為什麼不開玩笑?
React 的官方文檔推薦使用 https://facebook.github.io/jest/ 作為測試運行器和 React 測試框架。 Jest 基於 Jasmine 構建並使用相同的語法。 除了從 Jasmine 獲得的所有內容之外,Jest 還模擬除我們正在測試的組件之外的所有內容。 這在理論上很棒,但我覺得很煩人。 我們還沒有實現的任何東西,或者來自代碼庫的不同部分的東西,都只是undefined
。 雖然這在許多情況下都很好,但它可能會導致錯誤的悄悄失敗。
例如,我在測試點擊事件時遇到了麻煩。 無論我嘗試什麼,它都不會調用它的偵聽器。 然後我意識到這個函數被 Jest 嘲笑了,它從來沒有告訴我這個。
但到目前為止,Jest 最糟糕的問題是它沒有自動測試新更改的監視模式。 我們可以運行一次,得到測試結果,就是這樣。 (我喜歡在工作時在後台運行我的測試。否則我會忘記運行它們。)現在這不再是一個問題。
哦,Jest 不支持在多個瀏覽器中運行 React 測試。 這個問題比以前少了,但我覺得這是一個重要的功能,因為只有在特定版本的 Chrome 中才會出現 heisenbug 的罕見情況……
編者按:自從本文最初編寫以來,Jest 有了很大的改進。 您可以閱讀我們最近的教程,使用 Enzyme 和 Jest 進行反應單元測試,並自行決定 Jest 測試是否能夠勝任當今的任務。
React 測試:一個集成的例子
無論如何,我們已經看到了一個好的前端 React 測試在理論上應該如何工作。 讓我們用一個簡短的例子來付諸行動。
我們將使用由 React 和 d3.js 製作的散點圖組件來可視化生成隨機數的不同方式。 代碼及其演示也在 Github 上。
我們將使用 Karma 作為測試運行器,Mocha 作為測試框架,Webpack 作為模塊加載器。
設置
我們的源文件將放在<root>/src
目錄中,我們將把測試放在<root>/src/__tests__
目錄中。 這個想法是我們可以在src
中放置幾個目錄,每個主要組件一個目錄,每個目錄都有自己的測試文件。 像這樣捆綁源代碼和測試文件可以更輕鬆地在不同項目中重用 React 組件。
有了目錄結構,我們可以像這樣安裝依賴項:
$ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect
如果有任何安裝失敗,請嘗試重新運行安裝的該部分。 NPM 有時會以重新運行時消失的方式失敗。
完成後,我們的package.json
文件應如下所示:
// package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }
經過一些配置後,我們將能夠使用npm test
或karma start
運行測試。

配置
配置不多。 我們必須確保 Webpack 知道如何找到我們的代碼,並且 Karma 知道如何運行測試。
我們在./tests.webpack.js
文件中放置了兩行 JavaScript,以幫助 Karma 和 Webpack 協同工作:
// tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);
這告訴 Webpack 考慮將任何帶有-test
後綴的東西作為測試套件的一部分。
配置 Karma 需要更多的工作:
// karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };
這些行中的大多數來自默認的 Karma 配置。 我們使用browsers
表示測試應該在 Chrome 中運行,使用frameworks
來指定我們正在使用的測試框架,並使用singleRun
來讓測試默認只運行一次。 您可以使用karma start --no-single-run
讓 karma 在後台運行。
這三個很明顯。 Webpack 的東西更有趣。
因為 Webpack 處理我們代碼的依賴樹,所以我們不必在files
數組中指定所有文件。 我們只需要tests.webpack.js
,然後它需要所有必要的文件。
我們使用webpack
設置來告訴 Webpack 要做什麼。 在正常環境中,這部webpack.config.js
文件中。
我們還告訴 Webpack 為我們的 JavaScript 使用babel-loader
。 這為我們提供了來自 ECMAScript2015和React 的 JSX 的所有花哨的新功能。
通過webpackServer
配置,我們告訴 Webpack 不要打印任何調試信息。 它只會破壞我們的測試輸出。
一個 React 組件和一個測試
有了一個正在運行的測試套件,剩下的就很簡單了。 我們必須製作一個接受隨機坐標數組的組件,並創建一個帶有一堆點的<svg>
元素。
遵循 React 測試最佳實踐——即標準 TDD 實踐——我們將首先編寫測試,然後才是真正的 React 組件。 讓我們從src/__tests__/
中的普通測試文件開始:
// ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });
首先,我們需要 React、它的 TestUtils、d3.js、 expect
庫和我們正在測試的代碼。 然後我們用describe
創建一個新的測試套件,並創建一些隨機數據。
對於我們的第一個測試,讓我們確保ScatterPlot
呈現標題。 我們的測試進入describe
塊:
// ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });
大多數測試將遵循相同的模式:
- 使成為。
- 查找特定節點。
- 檢查內容。
正如我們之前看到的, renderIntoDocument
渲染我們的組件, findRenderedDOMComponentWithTag
找到我們正在測試的特定部分, getDOMNode
為我們提供原始 DOM 訪問。
起初我們的測試會失敗。 為了讓它通過,我們必須編寫呈現標題標籤的組件:
var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;
而已。 ScatterPlot
組件使用包含預期文本的<h1>
標籤呈現<div>
,我們的測試將通過。 是的,它不僅僅是 HTML,但請耐心等待。
畫出貓頭鷹的其餘部分
如上所述,您可以在 GitHub 上查看我們示例的其餘部分。 本文將跳過一步一步的描述,但大體過程與上面相同。 不過,我確實想向您展示一個更有趣的測試。 確保所有數據點都顯示在圖表上的測試:
// ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });
和之前一樣。 渲染,查找節點,檢查結果。 這裡有趣的部分是繪製那些 DOM 節點。 我們向ScatterPlot
組件添加一些 d3.js 魔法,如下所示:
// ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...
我們使用componentWillMount
為X和Y域設置空的 d3 比例,並使用componentWillReceiveProps
確保它們在發生變化時得到更新。 然後update_d3
確保為兩個比例設置domain
和range
。
我們將使用這兩個尺度在數據集中的隨機值和圖片上的位置之間進行轉換。 大多數隨機生成器返回[0,1]範圍內的數字,該範圍太小而無法視為像素。
然後我們將這些點添加到組件的 render 方法中:
// ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }
此代碼遍歷this.props.data
數組並為每個數據點添加一個<circle>
元素。 簡單的。
如果你想了解更多關於結合 React 和 d3.js 來製作數據可視化組件的信息,那是查看我的書React+d3.js的另一個重要原因。
自動化 React 組件測試:比聽起來容易
這就是我們使用 React 編寫可測試的前端組件所需要了解的全部內容。 要查看更多測試 React 組件的代碼,請查看 Github 上的 React 測試示例代碼庫,如上所述。
我們了解到:
- React 迫使我們模塊化和封裝。
- 這使得 React UI 測試很容易自動化。
- 單元測試對於前端來說是不夠的。
- 業力是一個偉大的測試跑步者。
- Jest 有潛力,但還沒有完全成熟。 (或者現在可能是。)
如果您喜歡這篇文章,請在 Twitter 上關注我並在下方發表評論。 感謝閱讀,祝 React 測試愉快!