React 測試入門教程

2021-03-02 前端大全

(點擊上方公眾號,可快速關注)

作者:阮一峰 

網址:http://www.ruanyifeng.com/blog/2016/02/react-testing-tutorial.html

越來越多的人,使用React開發Web應用。它的測試就成了一個大問題。

React的組件結構和JSX語法,不適用傳統的測試工具,必須有新的測試方法和工具。

本文總結目前React測試的基本做法和最佳實踐,手把手教你如何寫React測試。


一、Demo應用

請先安裝Demo。

$ git clone https://github.com/ruanyf/react-testing-demo.git

$ cd react-testing-demo

然後,打開 http://127.0.0.1:8080/,你會看到一個 Todo 應用。

接下來,我們就要測試這個應用,一共有5個測試點。

應用標題應為」Todos」

Todo項的初始狀態(」未完成」或」已完成」)應該正確

點擊一個Todo項,它就反轉狀態(」未完成」變為」已完成」,反之亦然)

點擊刪除按鈕,該Todo項就被刪除

點擊添加按鈕,會新增一個Todo項

這5個測試用例都已經寫好了,執行一下就可以看到結果。

$ npm test

下面就來看,測試用例應該怎麼寫。測試框架我用的是Mocha,如果你不熟悉,可以先讀我寫的《Mocha教程》。

二、測試工具庫

React測試必須使用官方的測試工具庫,但是它用起來不夠方便,所以有人做了封裝,推出了一些第三方庫,其中Airbnb公司的Enzyme最容易上手。

這就是說,同樣的測試用例至少有兩種寫法,本文都將介紹。

三、官方測試工具庫

我們知道,一個React組件有兩種存在形式:虛擬DOM對象(即React.Component的實例)和真實DOM節點。官方測試工具庫對這兩種形式,都提供測試解決方案。

3.1 Shallow Rendering

Shallow Rendering (淺渲染)指的是,將一個組件渲染成虛擬DOM對象,但是只渲染第一層,不渲染所有子組件,所以處理速度非常快。它不需要DOM環境,因為根本加載進DOM。

首先,在測試腳本之中,引入官方測試工具庫。

import TestUtils from 'react-addons-test-utils';

然後,寫一個 Shallow Rendering 函數,該函數返回的就是一個淺渲染的虛擬DOM對象。

import TestUtils from 'react-addons-test-utils';

 

function shallowRender(Component) {

  const renderer = TestUtils.createRenderer();

  renderer.render(<Component/>);

  return renderer.getRenderOutput();

}

第一個測試用例,是測試標題是否正確。這個用例不需要與DOM互動,不涉及子組件,所以使用淺渲染非常合適。

describe('Shallow Rendering', function () {

  it('App\'s title should be Todos', function () {

    const app = shallowRender(App);

    expect(app.props.children[0].type).to.equal('h1');

    expect(app.props.children[0].props.children).to.equal('Todos');

  });

});

上面代碼中,const app = shallowRender(App)表示對App組件進行」淺渲染」,然後app.props.children[0].props.children就是組件的標題。

你大概會覺得,這個屬性的寫法太古怪了,但實際上是有規律的。每一個虛擬DOM對象都有props.children屬性,它包含一個數組,裡面是所有的子組件。app.props.children[0]就是第一個子組件,在我們的例子中就是h1元素,它的props.children屬性就是h1的文本。

第二個測試用例,是測試Todo項的初始狀態。

首先,需要修改shallowRender函數,讓它接受第二個參數。

import TestUtils from 'react-addons-test-utils';

 

function shallowRender(Component, props) {

  const renderer = TestUtils.createRenderer();

  renderer.render(<Component {...props}/>);

  return renderer.getRenderOutput();

}

下面就是測試用例。

import TodoItem from '../app/components/TodoItem';

 

describe('Shallow Rendering', function () {

  it('Todo item should not have todo-done class', function () {

    const todoItemData = { id: 0, name: 'Todo one', done: false };

    const todoItem = shallowRender(TodoItem, {todo: todoItemData});

    expect(todoItem.props.children[0].props.className.indexOf('todo-done')).to.equal(-1);

  });

});

上面代碼中,由於TodoItem是App的子組件,所以淺渲染必須在TodoItem上調用,否則渲染不出來。在我們的例子中,初始狀態反映在組件的Class屬性(props.className)是否包含todo-done。

3.2 renderIntoDocument

官方測試工具庫的第二種測試方法,是將組件渲染成真實的DOM節點,再進行測試。這時就需要調用renderIntoDocument 方法。

import TestUtils from 'react-addons-test-utils';

import App from '../app/components/App';

 

const app = TestUtils.renderIntoDocument(<App/>);

renderIntoDocument 方法要求存在一個真實的DOM環境,否則會報錯。因此,測試用例之中,DOM環境(即window, document 和 navigator 對象)必須是存在的。jsdom 庫提供這項功能。

import jsdom from 'jsdom';

 

if (typeof document === 'undefined') {

  global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');

  global.window = document.defaultView;

  global.navigator = global.window.navigator;

}

將上面這段代碼,保存在test子目錄下,取名為 setup.js。然後,修改package.json。

{

  "scripts": {

    "test": "mocha --compilers js:babel-core/register --require ./test/setup.js",

  },

}

現在,每次運行npm test,setup.js 就會包含在測試腳本之中一起執行。

第三個測試用例,是測試刪除按鈕。

describe('DOM Rendering', function () {

  it('Click the delete button, the Todo item should be deleted', function () {

    const app = TestUtils.renderIntoDocument(<App/>);

    let todoItems = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li');

    let todoLength = todoItems.length;

    let deleteButton = todoItems[0].querySelector('button');

    TestUtils.Simulate.click(deleteButton);

    let todoItemsAfterClick = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li');

    expect(todoItemsAfterClick.length).to.equal(todoLength - 1);

  });

});

上面代碼中,第一步是將App渲染成真實的DOM節點,然後使用scryRenderedDOMComponentsWithTag方法找出app裡面所有的li元素。然後,取出第一個li元素裡面的button元素,使用TestUtils.Simulate.click方法在該元素上模擬用戶點擊。最後,判斷剩下的li元素應該少了一個。

這種測試方法的基本思路,就是找到目標節點,然後觸發某種動作。官方測試工具庫提供多種方法,幫助用戶找到所需的DOM節點。

scryRenderedDOMComponentsWithClass:找出所有匹配指定className的節點

findRenderedDOMComponentWithClass:與scryRenderedDOMComponentsWithClass用法相同,但只返回一個節點,如有零個或多個匹配的節點就報錯

scryRenderedDOMComponentsWithTag:找出所有匹配指定標籤的節點

findRenderedDOMComponentWithTag:與scryRenderedDOMComponentsWithTag用法相同,但只返回一個節點,如有零個或多個匹配的節點就報錯

scryRenderedComponentsWithType:找出所有符合指定子組件的節點

findRenderedComponentWithType:與scryRenderedComponentsWithType用法相同,但只返回一個節點,如有零個或多個匹配的節點就報錯

findAllInRenderedTree:遍歷當前組件所有的節點,只返回那些符合條件的節點

可以看到,上面這些方法很難拼寫,好在還有另一種找到DOM節點的替代方法。

3.3 findDOMNode

如果一個組件已經加載進入DOM,react-dom模塊的findDOMNode方法會返回該組件所對應的DOM節點。

我們使用這種方法來寫第四個測試用例,用戶點擊Todo項時的行為。

import {findDOMNode} from 'react-dom';

 

describe('DOM Rendering', function (done) {

  it('When click the Todo item,it should become done', function () {

    const app = TestUtils.renderIntoDocument(<App/>);

    const appDOM = findDOMNode(app);

    const todoItem = appDOM.querySelector('li:first-child span');

    let isDone = todoItem.classList.contains('todo-done');

    TestUtils.Simulate.click(todoItem);

    expect(todoItem.classList.contains('todo-done')).to.be.equal(!isDone);

  });

});

上面代碼中,findDOMNode方法返回App所在的DOM節點,然後找出第一個li節點,在它上面模擬用戶點擊。最後,判斷classList屬性裡面的todo-done,是否出現或消失。

第五個測試用例,是添加新的Todo項。

describe('DOM Rendering', function (done) {

  it('Add an new Todo item, when click the new todo button', function () {

    const app = TestUtils.renderIntoDocument(<App/>);

    const appDOM = findDOMNode(app);

    let todoItemsLength = appDOM.querySelectorAll('.todo-text').length;

    let addInput = appDOM.querySelector('input');

    addInput.value = 'Todo four';

    let addButton = appDOM.querySelector('.add-todo button');

    TestUtils.Simulate.click(addButton);

    expect(appDOM.querySelectorAll('.todo-text').length).to.be.equal(todoItemsLength + 1);

  });

});

上面代碼中,先找到input輸入框,添加一個值。然後,找到Add Todo按鈕,在它上面模擬用戶點擊。最後,判斷新的Todo項是否出現在Todo列表之中。

findDOMNode方法的最大優點,就是支持複雜的CSS選擇器。這是TestUtils本身不提供的。

四、Enzyme庫

Enzyme是官方測試工具庫的封裝,它模擬了jQuery的API,非常直觀,易於使用和學習。

它提供三種測試方法。

4.1 shallow

shallow方法就是官方的shallow rendering的封裝。

下面是第一個測試用例,測試App的標題。

import {shallow} from 'enzyme';

 

describe('Enzyme Shallow', function () {

  it('App\'s title should be Todos', function () {

    let app = shallow(<App/>);

    expect(app.find('h1').text()).to.equal('Todos');

  });

};

上面代碼中,shallow方法返回App的淺渲染,然後app.find方法找出h1元素,text方法取出該元素的文本。

關於find方法,有一個注意點,就是它只支持簡單選擇器,稍微複雜的一點的CSS選擇器都不支持。

component.find('.my-class'); // by class name

component.find('#my-id'); // by id

component.find('td'); // by tag

component.find('div.custom-class'); // by compound selector

component.find(TableRow); // by constructor

component.find('TableRow'); // by display name

4.2 render

render方法將React組件渲染成靜態的HTML字符串,然後分析這段HTML代碼的結構,返回一個對象。它跟shallow方法非常像,主要的不同是採用了第三方HTML解析庫Cheerio,它返回的是一個Cheerio實例對象。

下面是第二個測試用例,測試所有Todo項的初始狀態。

import {render} from 'enzyme';

 

describe('Enzyme Render', function () {

  it('Todo item should not have todo-done class', function () {

    let app = render(<App/>);

    expect(app.find('.todo-done').length).to.equal(0);

  });

});

在上面代碼中,你可以看到,render方法與shallow方法的API基本是一致的。 Enzyme的設計就是,讓不同的底層處理引擎,都具有同樣的API(比如find方法)。

4.3 mount

mount方法用於將React組件加載為真實DOM節點。

下面是第三個測試用例,測試刪除按鈕。

import {mount} from 'enzyme';

 

describe('Enzyme Mount', function () {

  it('Delete Todo', function () {

    let app = mount(<App/>);

    let todoLength = app.find('li').length;

    app.find('button.delete').at(0).simulate('click');

    expect(app.find('li').length).to.equal(todoLength - 1);

  });

});

上面代碼中,find方法返回一個對象,包含了所有符合條件的子組件。在它的基礎上,at方法返回指定位置的子組件,simulate方法就在這個組件上觸發某種行為。

下面是第四個測試用例,測試Todo項的點擊行為。

import {mount} from 'enzyme';

 

describe('Enzyme Mount', function () {

  it('Turning a Todo item into Done', function () {

    let app = mount(<App/>);

    let todoItem = app.find('.todo-text').at(0);

    todoItem.simulate('click');

    expect(todoItem.hasClass('todo-done')).to.equal(true);

  });

});

下面是第五個測試用例,測試添加新的Todo項。

import {mount} from 'enzyme';

 

describe('Enzyme Mount', function () {

  it('Add a new Todo', function () {

    let app = mount(<App/>);

    let todoLength = app.find('li').length;

    let addInput = app.find('input').get(0);

    addInput.value = 'Todo Four';

    app.find('.add-button').simulate('click');

    expect(app.find('li').length).to.equal(todoLength + 1);

  });

});

4.4 API

下面是Enzyme的一部分API,你可以從中了解它的大概用法。

.get(index):返回指定位置的子組件的DOM節點

.at(index):返回指定位置的子組件

.first():返回第一個子組件

.last():返回最後一個子組件

.type():返回當前組件的類型

.text():返回當前組件的文本內容

.html():返回當前組件的HTML代碼形式

.props():返回根組件的所有屬性

.prop(key):返回根組件的指定屬性

.state([key]):返回根組件的狀態

.setState(nextState):設置根組件的狀態

.setProps(nextProps):設置根組件的屬性

【今日微信公號推薦↓】

相關焦點

  • React Hook 入門教程
    React Hook 入門教程Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。react hook 產生的原因在組件之間復用狀態邏輯很難組件之間復用狀態邏輯很困難,我們可能會把組件連結到我們的 store 裡面。
  • Redux 入門教程(三):React-Redux 的用法
    前兩篇教程介紹了 Redux 的基本用法和異步操作,今天是最後一部分,介紹如何在 React 項目中使用 Redux。
  • React Status 中文周刊 #15 - 全新的 「Redux 核心基礎」 教程
    Redux 官方團隊重寫了入門教程部分,加入了最佳實踐和代碼演示。大家可以去官網一探究竟。
  • ReactNative學習資源大匯集
    目錄教程React Native構建 Facebook F8 2016 App / React Native 開發指南 http://f8-app.liaohuqiu.net/React-Native入門指南 https://github.com/vczero/react-native-lesson30天學習React Native教程 https://github.com
  • React Status 中文周刊 #2 - 使用 React 編寫 CLI 工具
    這篇教程將帶你解決學習它遇到的難題。Lea Marchand📘 教程與趣事如何使用 React 編寫一個搜索組件 — 大家都喜歡以解決常見編程問題的一手教程Jozef PetroReact Native 入門(2020 年) — 我們會時不時的回憶一下之前可能使用過的一些技術
  • React Status 中文周刊 #8 - 100 行代碼實現 Facebook 的 Recoil React 庫
    Facebook Experimental📘 教程和趣事React 代碼分割入門指南 Mohammad Ashour如何在 React Native 中使用 Expo — 兩篇系列文章,第一篇文章作者提供一個簡單易懂的入門教程
  • React Status 中文周刊 #26 - Aleph:基於 Deno 的 React 框架
    v=TNhaISOUy6QFireship📘 教程與趣事優化 React PWA 應用的性能 — 本文中的案例由作者接手改造一個快被遺棄的項目開始,隨後,作者詳細說明了如何通過著眼於可量化的改進來解決問題。
  • React系列教程
    2.腳手架的介紹3.認識React的腳手架 - create-react-app4.腳手架的環境的下載及使用5.腳手架內部的基本構成介紹6.使用腳手架製作圖片展示組件 第十三講:React的動畫1.React動畫的基本使用2.React的動畫插件 - react-transition-group
  • react腳手架create-react-app入門
    記得關注,每天都在分享不同知識不管是哪個前端框架都支持快速開發,通過腳手架可以讓咱們更便捷的構建項目,無需考慮搭建環境問題,今天來聊聊create-react-app腳手架安裝腳手架>npm install -g create-react-app創建項目create-react-app myapp # myapp是項目的名稱,這樣就會在當前目錄生成一個myapp的項目
  • React Status 中文周刊 #21 - 2020 年 React 大事記回顧
    停止使用 `fetch` 進行 mock — 作者不僅解釋了為什麼不應在測試中使用 fetch 進行 mock,同時給出了代替方案。原文連結:https://www.robinwieruch.de/react-hooks-higher-order-components原文連結:https://www.robinwieruch.de/react-hooks-higher-order-components原文連結:https://www.robinwieruch.de/react-hooks-higher-order-components
  • TypeScript 中文入門教程
    鑑於我的博客的影響力(羞),比如在百度上搜索: typescript 入門,我之前的文章是結果的第三個(2015-12-03測試),所以我有必要將這些翻譯 Copy 到我的博客,然後整理一下發表到博客園主頁,當然我會註明這不是我翻譯的,註明出處,這樣至少通過我的博客可以跳轉到原始的翻譯。於是我繼續下去了。
  • React.js 2016最佳實踐
    如果你只是剛開始接觸React.js,請閱讀React.js教程(https://blog.risingstack.com/the-react-way-getting-started-tutorial/),或Pete Hunt的React howto(https://github.com/petehunt/react-howto)。
  • Jmeter性能測試 入門
  • React SSR 同構入門與原理
    執行命令: create-react-app react-csr 創建一個 React SPA 單頁面應用項目 。執行命令: npm run start 啟動項目。官網解釋查看最終效果: 如果對於 React Router 不熟悉的同學可以查看作者的>>>React Router 入門與原理本小結完整代碼地址>>>點擊查看兼容 ReduxRedux
  • 正則表達式入門教程 + 免費在線正則測試工具推薦
    如果你還處於入門學習階段,單靠腦子憑空寫一些複雜的正則,很難保證準確性,後面往往要花費大量時間去調試。其實,藉助一些可視化的正則測試工具網站,不僅能節省大量時間,還能對正則有更深的理解……什麼是正則表達式?
  • React Status 中文周刊 #19 - React Hook發布兩周年回顧
    原文連結:https://dev.to/ryansolid/the-react-hooks-announcement-in-retrospect-2-years-later-18lm原文連結:https://react-spectrum.adobe.com/react-aria/index.htmlAdobe
  • 「首席架構師推薦」關於React生態系統的一系列精選資源(3)
    reactn - React,但內置全局狀態管理immer - 通過改變當前狀態來創建下一個不可變狀態地圖react-googlemaps - 反映Google地圖的界面react-maps - React的映射組件react-google-maps - React.js Google Maps集成組件react-gmaps - React.js的Google Maps組件react-map-gl
  • 如何入門網站安全滲透測試
    從大學畢業的時候開始簡單入門,寫寫網站程序代碼,搞搞sql注入以及安全測試,到現在Sinesafe當安全工程師,差不多在安全行業成長了11年,發現不懂得問題隨著實戰滲透測試中非常多,還是學到老乾到老才是成功之道。
  • React組件測試虛擬DOM的方法
    淺渲染使用react-addons-test-utils模塊的createRenderer()函數創建一個渲染器,渲染器渲染組件並緩存渲染出的虛擬DOM節點,通過調用渲染器的getRenderOutput()函數獲得虛擬DOM對象。
  • 入門Web開發者福音 React學習少走彎路的實用貼
    但是學好React並非易事,特別是對於剛剛入門Web開發的人來說。從一張由adam-golab創作超詳細的React開發者學習路線圖,開啟今天的知識分享貼。基礎知識HTML、CSS和JavaScript,這三者是Web開發的三大支柱。HTMLHTML是第一根支柱,是Web開發者最重要的技能,它提供了網頁的結構。