1.為什么要使用單元測試工具?
因為代碼之間的相互調用關系,又希望測試過程單元相互獨立,又能正常運行,這就需要我們對被測函數的依賴函數和環境進行mock,在測試數據輸入、測試執行和測試結果檢查方面存在很多相似性,測試工具正是為我們在這些方面提供了方便。
所謂單元測試也就是對每個單元進行測試,通俗的將一般針對的是函數,類或單個組件,不涉及系統和集成。單元測試是軟件測試的基礎測試。
2.React 的標配測試工具 Jest。
Jest主要有以下特點:
1.適應性:Jest是模塊化、可擴展和可配置的。
2.沙箱和快速:Jest虛擬化了JavaScript的環境,能模擬瀏覽器,並且並行執行
3.快照測試:Jest能夠對React 樹進行快照或別的序列化數值快速編寫測試,提供快速更新的用戶體驗。
4.支持異步代碼測試:支持promises和async/await
5.自動生成靜態分析結果:不僅顯示測試用例執行結果,也顯示語句、分支、函數等覆蓋率。
JEST對比Mocha來說,因為如下幾個優點最后勝出:
1.和React師出同門,FB官方支持
2.已經集成了測試覆蓋率檢查、mock等功能,不需要安裝額外的庫
3.文檔完備,官方提供了和babel、webpack集成情況下以及異步調用的測試解決方案
4.官方提供snapshot testing解決方案
3.Jest + Enzyme的使用過程
1.安裝
$ nam install —save-dev jest
如果需要在測試項目中使用babel,需要安裝babel-jest
$ nam install —save-dev babel-jest
然后安裝enzyme
$ npm install enzyme —save-dev
如果使用的react版本在13以上,還需要安裝react-addons-test-utils
$ nam i —save-dev react-addons-test-utils
2.配置
JEST運行基礎功能雖然無需配置,但是官方依然提供了配置選項來實現個性化需求。
package.json文件中配置jest的collectCoverageFrom參數,來指定檢查所有需要測試的文件(無論源文件有沒有被測試文件使用到)
coverageThreshold 參數來配置測試覆蓋率。
jest.config.js 配置jest:
module.exports = {
bail: true, //遇上 test feature, 則Stop running test, 默認值是false
cacheDirectory: './node_modules/.cache', //測試緩存數據的存儲位置
testEnvironment: 'jsdom', //default brower-like enviroment, 如果你搭建了一個node service node-like enviroment
coverageThreshold: { //測試覆蓋率, 閾值不滿足,就返回測試失敗
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
coveragePathIgnorePatterns: [ //該路徑下的測試,忽略在測試覆蓋率上
'build',
'<rootDir>/src/shared/libs/url/',
],
testRegex: 'test/.*\\.jsx?$', //要測試的文件目錄及后綴
testPathIgnorePatterns: [ //忽略該路徑的文件測試
'<rootDir>/node_modules/',
'<rootDir>/build/',
'<rootDir>/scripts/',
'<rootDir>/api/',
'<rootDir>/test/setup.js',
'__mocks__',
],
moduleFileExtensions: ['', 'json', 'js', 'jsx', 'less'], //測試模塊中用到的文件的后綴名配置
modulePaths: ['<rootDir>/src', '<rootDir>'],
moduleNameMapper: { //與測試無關的資源文件同意mock 掉,這樣在import 的時候就不會真的引入這些文件
'^import?': '<rootDir>/build/jestImportMock.js',
'\\.(css|less|gif|jpg|jpeg|png)$': '<rootDir>/build/jestStyleMock.js',
},
setupFiles: ['<rootDir>/test/setup.js'], //給每個測試文件添加額外的配置
transformIgnorePatterns: [ //測試過程不改變滿足配置的文件
'<rootDir>/node_modules/(?!(react-aaui|tempest\\.js)/)',
'babel-runtime',
],
}
4.了解React官方測試工具庫
react測試可以分為測試DOM結構 和測試Action和Reducer
React官方測試工具庫提供兩種測試形式:
1.Shallow Rendering 測試虛擬DOM的方法
Shallow Rendering (淺渲染)指的是,將一個組件渲染成虛擬DOM對象,但是只渲染第一層,不渲染所有子組件,所以處理速度非常快。它不需要DOM環境,因為根本沒有加載進DOM。
import TestUtils from 'react-addons-test-utils';
function shallowRender(Component) {
const renderer = TestUtils.createRenderer();
renderer.render(<Component/>);
return renderer.getRenderOutput();
}
Shallow Rendering 函數,該函數返回的就是一個淺渲染的虛擬DOM對象。只有一層,不返回子組件。
2.DOM Rendering 測試真實DOM的方法
官方測試工具庫的第二種測試方法,是將組件渲染成真實的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 對象)必須是存在的
Enzyme庫對官方測試庫進行了封裝,它提供三種方法:
import { shallow, mount, render } from ‘enzyme’
shallow 返回組件的淺渲染,對官方shallow rendering 進行封裝
const wrapper = shallow(<Counter {...props} />)
expect(wrapper.find('button').exists()).toBeTruthy()
shallow 返回Counter 的淺渲染,然后調用find 方法查找 button 元素
關於find方法,有一個注意點,就是它只支持簡單選擇器,稍微復雜的一點的CSS選擇器都不支持。
render 方法將React組件渲染成靜態的HTML字符串,然后分析這段HTML代碼的結構,返回一個對象。它跟shallow方法非常像,主要的不同是采用了第三方HTML解析庫Cheerio,它返回的是一個Cheerio實例對象。
const wrapper = render(<Counter {...props} />)
expect(wrapper.find('button').exists()).toBeTruthy()
render方法與shallow方法的API基本是一致的。
Enzyme的設計就是,讓不同的底層處理引擎,都具有同樣的API
mount 方法用於將React組件加載為真實DOM節點。
const wrapper = mount(<Counter arithmetic={arithmetic})
wrapper.find('button').simulate('click')
Enzyme的一部分API,你可以從中了解它的大概用法。
.get(index):返回指定位置的子組件的DOM節點
.at(index):返回指定位置的子組件
.first():返回第一個子組件
.last():返回最后一個子組件
.type():返回當前組件的類型
.text():返回當前組件的文本內容
.html():返回當前組件的HTML代碼形式
.props():返回根組件的所有屬性
.prop(key):返回根組件的指定屬性
.state([key]):返回根組件的狀態
.setState(nextState):設置根組件的狀態
.setProps(nextProps):設置根組件的屬性
toMatchSnapshot方法會去幫你對比這次將要生成的結構與上次的區別
測試 異步action
他的I/O可能依賴store.getState(),自身又會依賴異步中間件,這類使用原生js測試起來比較困難,我們的目的可以設定為:當我們觸發一個action后,它經歷了一個圈異步最終store.getAction中的action拿到的數據和我們預期一致。因此我們需要用到兩個庫:redux-mock-store 和 nock。
const mockStore = configureStore([thunk, promiseMiddleware]) //配置mock 的store,讓他們有相同的middleware
afterEach(() =>
nock.cleanAll()
) //每執行完一個測試后,清空nock
const store = mockStore({
router: {
location: '/',
},
}) //以我們約定的初始state創建store,控制 I/O 依賴
const data = [
//接口返回的信息
{…
},
]
nock(API_HOST) //攔截請求返回的response
.get(`/api/…`) //拼接路由,需要在test.js中配置測試路徑
.reply(200, {code: 0, data})
return store.dispatch(actions.getAll()).then(() => expect(store.getActions()).toMatchSnapShot())
1.用nock來mock攔截http請求結果,並返回我們給定的response
2.用redux-mock-store 來mock store 的生命周期,需要預先把middleware配成和項目一致
3.describe會包含一些生命周期的api,比如全部測試開始做啥,單個測試結束做啥api,這里每執行完一個測試就清空nock
4.用了jest中的toMatchSnapShot api 來判斷兩個條件是否一致。
本來你要寫成 expect(store.getActions()).toEqual({data …}) 你需要把equal 里的東西都描寫具體,而toMatchSnapshot
可在當前目錄下生成一個snapshot ,專門存放當前結果,寫測試時看一眼是預期的就commit。如果改壞了,函數就不匹配snapshot了。
5.測試注意事項
1.拆分單元,關注輸入輸出,忽略中間過程。dom測試時只用確保正確調用了action函數,傳參正確,而不用關注函數調用結果,置於action處理結果,reducer中對state的改變這些都留給action和reducer自己的單元測試區測。不要想着測試整個大功能的流程,不要有閉環的思想,單元測試需要保證的當前單元正常,對於每個單元模塊輸入輸出都正確,理論串聯后一起使用閉環時也會正確。
2.多種情況的測試覆蓋,如果不能保證測試的全面性,每種情況都覆蓋到,那么這個測試就是個不敢依靠的不全面的測試。當然在實際項目中,可能因為時間、資源等問題,無法保證每種情況都測試到,而只測試主要的內容,這時候要做到心里有數,反正我是對於每個測試都寫注釋的,交代清楚測試覆蓋了哪些,還有哪些沒有覆蓋,需要其他手段保持穩定性。
3.關注該關注的,無關緊要的mock掉。css、圖片這種mock掉,http請求mock掉
4.原本不利於測試的代碼還是需要修改的,並不能為了原代碼穩定不變,在測試時不敢動原代碼。譬如函數不純,沒有返回值等。