React 測試入門教程

rgbu2525 8年前發布 | 20K 次閱讀 React 測試技術 JavaScript開發

來自: 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 && npm install
$ npm start

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

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

  1. 應用標題應為"Todos"
  2. Todo項的初始狀態("未完成"或"已完成")應該正確
  3. 點擊一個Todo項,它就反轉狀態("未完成"變為"已完成",反之亦然)
  4. 點擊刪除按鈕,該Todo項就被刪除
  5. 點擊添加按鈕,會新增一個Todo項

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

$ npm test

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

二、測試工具庫

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

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

  • 官方測試工具庫的寫法
  • Enzyme的寫法

三、官方測試工具庫

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

  • Shallow Rendering :測試虛擬DOM的方法
  • DOM Rendering : 測試真實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節點。

可以看到,上面這些方法很難拼寫,好在還有另一種找到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,非常直觀,易于使用和學習。

它提供三種測試方法。

  • shallow
  • render
  • mount

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) :設置根組件的屬性

(完)

</div>

 本文由用戶 rgbu2525 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!