写点什么

携程租车 React Native 单元测试实践

  • 2020-01-23
  • 本文字数:5226 字

    阅读完需:约 17 分钟

携程租车React Native单元测试实践

在较大规模的前端项目中,测试对于保证代码质量十分重要,而 React 的组件化和函数式编程, 这种相同输入一定返回相同输出的幂等特性特别适合单元测试。本篇即是 React 和 React Native 项目单元测试的完整方案介绍。

一、技术选型: Jest + Enzyme + react-hooks-testing-library

1.1 jest

Jest 是 FaceBook 出品的前端测试框架,适合用于 React 和 React Native 的单元测试。


有以下几个特点:


  • 简单易用:易配置,自带断言库和 mock 库。

  • 快照测试:能够创造一个当前组件的渲染快照,通过和上次保存的快照进行比较,如果两者不匹配说明测试失败。

  • 测试报告:内置了 Istanbul,通过一定配置可以测试代码覆盖率,生成测试报告。

1.2 Enzyme

Enzyme 是 AirBnb 开源的 React 测试工具库,通过一套简洁的 api,可以渲染一个或多个组件,查找元素,模拟元素交互(如点击,触摸),通过和 Jest 相互配合可以提供完整的 React 组件测试能力。

二、环境配置

直接贴上所需要安装的依赖:


"devDependencies": {       "@testing-library/react-hooks": "^3.2.1",  //React Hooks测试支持,仅支持React 16.9.0以上    "babel-jest": "^24.8.0",    "enzyme": "^3.10.0",    "enzyme-adapter-react-16": "^1.14.0", //依据对应React版本安装,React 15需安装enzyme-adapter-react-15    "jest": "^24.8.0",    "jest-junit": "^7.0.0",    "jest-react-native": "^18.0.0", //RN支持,非RN可以不装     "react-test-renderer": "16.9.0",     "redux-mock-store": "^1.5.3" //Redux测试模拟store}
复制代码


根目录下添加 jest.config.js 文件作为配置文件:


module.exports = {  preset: 'react-native',  globals: { //模拟的全局变量    _window: {},    __DEV__: true,  },  setupFiles: ['./jest.setup.js'], //运行测试前需运行的初始化文件,例子在下方  moduleNameMapper: { //需要模拟的静态资源    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':    "\\.(css|less|scss)$": "<rootDir>/__mocks__/stylesMock.js"  },  transform: { //转译配置,RN项目配置如下,普通React项目可以使用babel-jest    '^.+\\.js$': '<rootDir>/node_modules/react-native/jest/preprocessor.js',  },  testMatch: ['**/__tests__/**/*.(spec|test).js'],//正则匹配的测试文件  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],  unmockedModulePathPatterns: ['<rootDir>/node_modules/react'],  collectCoverage: true,  collectCoverageFrom: [//生成测试报告时需覆盖测试的文件    'src/**/*.js',  ],  coverageReporters: ['text-summary', 'json-summary', 'lcov', 'html', 'clover'],  testResultsProcessor: './node_modules/jest-junit',  transformIgnorePatterns: ['<rootDir>/node_modules/(?!@ctrip|react-native)'], //transform白名单};
复制代码

三、Jest 简单函数单元测试

待测试函数


function add(x, y) {    return x + y;}
复制代码


测试文件


  test('should return 3', () => {    const x = 1;    const y = 2;    const output = 3;    expect(add(x, y)).toBe(output);  });});
复制代码


  • describe:创造一个块,将一组相关的测试用例组合在一起

  • test:也可以用 it,测试用例

  • expect:使用该函数断言某个值


常用断言


  • toBe:测试是否完全相等

  • toBeCloseTo:浮点数比较

  • toEqual:对象深度比较

  • not:取反

  • toBeNull:匹配 null

  • toBeUndefined:匹配 undefined

  • toBeDefined:与 toBeUndefined 相反

  • toBeTruthy:匹配真

  • toBeFalsy:匹配假

  • toBeGreaterThan:大于

  • toBeGreaterThanOrEqual:大于等于

  • toBeLessThan:小于

  • toBeLessThanOrEqual:小于等于

  • toMatch:正则表达匹配

  • resolves/reject:测试 promise

  • toBeCalled:函数是否被调用

  • toBeCalledWith:函数是否以某些参数为入参被调用

  • assertions:检测用例中有多少个断言被调用,一般用于异步测试

四、Jest 周期函数

在写测试用例之前,可以用四个周期函数进行一些处理:


beforeAll(() => {  console.log('所有测试用例测试之前运行');});
afterAll(() => { console.log('所有测试用例测试完毕后运行');});
beforeEach(() =>{ console.log('每个测试用例测试之前运行');});
afterEach(() => { console.log('每个测试用例测试完毕后运行');});
复制代码

五、Jest Mock 函数

在单元测试中,有许多对象或函数并不需要真实的引用,因此需要 mock。比如之前提到的初始化文件 jest.setup.js 中,我们会 mock 一些对象:


jest.useFakeTimers(); //mock时间
jest.mock('./src/commons/CViewPort', () => { //mock一些组件 return props => { return <View {...props}>{props && props.children}</View>; };});
jest.mock('./src/commons/CToast', () => { return { show: () => {}, };});
复制代码


也可以手动 mock 一些 React Native 组件,在根目录下建立 mocks 文件夹。文件下建立需要 mock 的组件的文件,如建立 InteractionManager.js。


const InteractionManager = {  runAfterInteractions: callback => callback(),};
module.exports = InteractionManager;
复制代码


建立好文件后,这样 mock 即可:


jest.mock('InteractionManager');
复制代码

六、Jest UI 快照测试

Jest 提供了 snapshot 快照功能用于 UI 测试,可以创建组件的渲染快照并将其与以前保存的快照进行比较,如果两者不匹配,则测试失败。快照将在测试文件的当前文件路径自动生成的 snapshots 文件夹中保存。当主动修改造成 ui 变化时,使用 jest -u 来更新快照。


it('render List', () => {  const tree = renderer.create(<List {...props} />).toJSON();  expect(tree).toMatchSnapshot();});
复制代码


快照不匹配:


七、Jest 异步测试

Jest 单元测试是同步的,因此面对异步操作如 fetch 获取数据,需要进行异步的模拟测试。首先,对 fetch 函数进行 mock:


const cityInfo = {    1: '北京',    2: '上海'}
export default function fetch(url, params) { return new Promise((resolve, reject) => { if (params.cityId && cityInfo[params.cityId]) { resolve(cityInfo[params.cityId]); } else { reject('city not found'); } });}
复制代码


接着创建测试用例进行异步测试:


it('test cityInfo', async () => {  expect.assertions(1); //检测用例中有多少个断言被调用  const data = await fetch('/cityInfo', {cityId: 1});  expect(data).toEqual('北京');});
复制代码

八、Enzyme 组件测试

import { mount, shallow, render } from ‘enzyme';
复制代码


Enzyme 对测试组件进行渲染分为三种:


  • shallow:浅渲染,仅渲染单个组件,不包括其子组件。这对于隔离组件进行纯单元测试很有用,效率高,可以进行模拟交互,并且从 Enzyme 3 开始也可以访问组件生命周期,所以一般组件测试用 shallow 即可。

  • mount:完整渲染,包括其子组件。因为渲染了真实的 DOM 节点,可以用来测试 DOM API 的交互和组件的生命周期。

  • render:静态渲染,渲染为静态 HTML 字符串,包括子组件,不能访问生命周期,不能模拟交互。

8.1 测试组件模拟交互

const onClickLabel = jest.fn();const label = shallow(<Label filterData={filterData} onClickLabel={onClickLabel} />);
label.childAt(0).find({ eventName: 'click filterLabel' }).simulate('press');expect(onClickLabel).toBeCalled();
复制代码

8.2 测试组件内部方法

const fliterModal = shallow(<FilterModal {...props} />);const instance = fliterModal.instance(); //获取当前组件实例
//jest.spyOn创建一个mock函数,该mock函数不仅捕获函数的调用情况,还可以正常的执行被spy的函数。jest.spyOn(instance, '_onClear');
instance.forceUpdate();
fliterModal.childAt(0).simulate('press');expect(instance._onClear).toBeCalled();//测试组件实例上的方法是否被调用
复制代码

九、Redux 测试

在使用 React 或者 React Native 时通常会使用 Redux 进行状态的管理,需要 mock store 进行测试。


import configureMockStore from 'redux-mock-store';import thunk from 'redux-thunk';import { updateList } from '../pages/List/action';
const middlewares = [thunk];//引入redux-mock-store 对store进行mockconst mockStore = configureMockStore(middlewares);
describe('list action test', () => { it('updateList test', () => { const store = mockStore({ flist: {} }); const mockData = { flist: { afitem: 1 } };
const expectedActions = { type: 'UPDATE_LIST', flist: { afitem: 1 }};
expect(store.dispatch(updateList(mockData.flist))).toEqual(expectedActions); });});
复制代码

十、React-Hooks 单元测试

在 React Native v0.59 版本以后,RN 也支持了 React Hooks 的开发,由于 Enzyme 对于 Hooks 的测试支持不理想,我们专门引入了 react-hooks-testing-library 用于 Hooks 的测试。

10.1 安装

npm install --save-dev @testing-library/react-hooks
复制代码

10.2 useState 测试

// useCityName.jsimport { useState, useCallback } from 'react';export default function useCityName() {  const [cityName, setCityName] = useState('北京');  const format = useCallback(() => setCityName(x => x + '市'), []);  return { cityName, format };}

// useCityName.test.jsdescribe('test useCityName', () => { it('should use cityname', () => { const { result } = renderHook(() => useCityName()); expect(result.current.cityName).toBe('北京'); expect(typeof result.current.format).toBe('function'); });
it('should format cityname', () => { const { result } = renderHook(() => useCityName()); act(() => { result.current.format(); }); expect(result.current.cityName).toBe('北京市'); });});
复制代码

10.3 useEffect 测试

// useCityInfo.jsimport { useEffect } from 'react';
export default function useCityInfo({ cityInfo, id }) { useEffect(() => { cityInfo[id] = '北京'; return () => { cityInfo[id] = '上海'; }; }, [id]);}// useCityInfo.test.jsdescribe('test useCityName', () => { it('should handle useEffect hook', () => { const cityInfo = { 1: '北京', 2: '上海', };
const { sideEffect, unmount } = renderHook(useCityInfo, { initialProps: { cityInfo, id: 1 } });
sideEffect({ cityInfo, id: 1 });
expect(cityInfo[1]).toBe('北京');
sideEffect({ cityInfo, id: 2 });
expect(cityInfo[2]).toBe('北京');
unmount();
expect(cityInfo[1]).toBe('上海'); expect(cityInfo[2]).toBe('上海'); });});
复制代码

十一、单元测试覆盖率及 husky 做代码提交检查

Jest 集成了 Istanbul 这个代码覆盖工具并会生成详细报告,执行 jest --coverage 即可生成基于四个维度的覆盖率报告:



  • 语句覆盖率(statement)

  • 分支覆盖率(branches)

  • 函数覆盖率(functions)

  • 行覆盖率(lines)


同时我们会配置 husky 在 commit 或者 push 之前添加钩子,在这些动作之前强制执行单元测试,通过测试才可提交到远程代码仓库以保证代码质量。


husky 在 package.json 中的配置:


"scripts": {,    "test": "jest --forceExit --silent"},"devDependencies": {    "husky": "^3.0.9"},"husky": {    "hooks": {        "pre-push": "npm run test"    }},
复制代码

十二、总结

本篇是 React Native 项目单元测试的一个简单教程,在携程的持续集成流程中再接入 sonar, 可以查看完整的单元测试报告。


在携程租车前端单元测试的实践中,我们总结出几个要点:


  • 将待测试的组件当成黑盒,不用考虑内部逻辑实现;

  • UI 改动频繁,优先保证公用组件,工具函数,核心代码的单元测试;

  • 模拟数据尽量真实;

  • 多考虑边界条件情况;


通过单元测试,给项目带来了不少好处:


  • 通过单元测试可以确保代码得到预期的结果,在测试环境中就发现 bug;

  • 当修改依赖的组件时,能在测试中发现被影响组件的错误,这样可以支持我们更好的重构代码,有利于项目的长期迭代;

  • 良好的单元测试就是一份最好的注释,同时迫使我们写易于测试的函数式代码;


另外我们在写单元测试的时候并不是堆砌覆盖率,而是需要保证功能细节的正确,覆盖率并不是最重要的,单元测试也不是银弹,我们也在结合诸如 airtest 自动化测试等其他测试和手段保证代码的质量。


作者介绍


琨玮,携程高级前端开发工程师,从事 React Native/Web 前端的开发及维护工作,喜欢研究新技术。


本文转载自公众号携程技术(ID:ctriptech)。


原文链接


https://mp.weixin.qq.com/s?__biz=MjM5MDI3MjA5MQ==&mid=2697269321&idx=2&sn=d8f9855436b9fa38a1674781a383fcdf&chksm=8376ef7db401666bc73060add2c7c28e0dd63462f9ada06dfc4b54e31870cc44ec183d315a64&scene=27#wechat_redirect


2020-01-23 09:454425

评论 1 条评论

发布
用户头像
const data = await fetch('/cityInfo', {cityId: 1}); 不能用await吧?
2020-08-05 16:39
回复
没有更多了
发现更多内容

全面!2020华为Android岗面试真题(已解析含答案,android蓝牙开发框架

android 程序员 移动开发

关于MVC_MVP_MVVM的一些错误认识,android面试流程

android 程序员 移动开发

毕业设计—电商秒杀系统

俊杰

架构实战营

兄弟们,这年头,咱移动客户端工程师还有前途吗,flutter图片压缩上传

android 程序员 移动开发

做了5年Android,靠着这份190页的面试资料,成功入职腾讯

android 程序员 移动开发

入职两年的安卓“程序员“跳槽,2021年阿里Android面试题精选

android 程序员 移动开发

全面理解 Flutter(万字长文,深度解析,整理了3家面试问题:美团+字节+腾讯

android 程序员 移动开发

关于 Flutter 是不是“大有可为”这件事,androidjetpack教程

android 移动开发

关于拼多多被曝删除用户本机照片的一点想法,移动服务框架app下载安装

android 程序员 移动开发

MySQL锁的分类知多少

卢卡多多

MySQL锁 11月日更

关于拼多多被曝删除用户本机照片的一点想法(1),android程序设计基础

android 程序员 移动开发

做了5年Android,靠着这份面试题跟答案,我从12K变成了30K

android 程序员 移动开发

架构实战营模块毕业总结

河马先生

架构实战营

关于使用 Android MVVM + LiveData 模式的一些建议,ffmpeg音视频同步

android 程序员 移动开发

区块链上的房地产:区块链会颠覆房地产吗?

CECBC

架构实战营毕业设计

宁静志远

做了3年大厂HR,这几种程序员我会直接pass掉!,网站开发前后端分离

android 程序员 移动开发

架构实战营-模块九-毕业设计

Cingk

关于大厂Android面试必问的事件分发机制,应该没有比这篇讲的更好的了

android 程序员 移动开发

做Android开发,如何使用 Kotlin 提高生产力!,android开发前景2019

android 程序员 移动开发

六年Android从迷茫到大牛的成长之路,不忘初心,方得始终

android 程序员 移动开发

全面复盘Android开发者容易忽视的Backup功能 _ 创作者训练营第二期

android 程序员 移动开发

关于Android的渲染机制,大厂面试官最喜欢问的7个问题【建议收藏

android 程序员 移动开发

关于程序员35岁的坎:年龄不是挡板,当你匹配了这个年纪该有的能力还有什么畏惧

android 程序员 移动开发

这本“算法宝典”讲得透彻,完全掌握后,我竟拿到字节跳动offer

程序员 算法 字节

做了5年Android,靠着这份190页的面试资料,成功入职字节跳动

android 程序员 移动开发

全网都刷爆了,不会只有你不知道吧—,android智能手机编程答案

android 程序员 移动开发

内存泄漏以优化大全,2021非科班生的Android面试之路

android 程序员 移动开发

元宇宙,如何看待它就是下一代互联网(附下载)

CECBC

再见!onActivityResult!你好(1),太现实了

android 程序员 移动开发

做了六年Android,终于熬出头了,15K到31K全靠这份高级面试题

android 程序员 移动开发

携程租车React Native单元测试实践_软件工程_琨玮_InfoQ精选文章