HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

携程租车 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:454264

评论 1 条评论

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

软件测试丨JavaScript脚本注入,完成Selenium 无法做到的那些事

测试人

JavaScript 软件测试 自动化测试 测试开发 selenium

飞针测试的流程有哪些?华秋一文告诉你

华秋电子

TiDB Operator常见问题和解决步骤(一)

TiDB 社区干货传送门

实践案例 集群管理 管理与运维 故障排查/诊断

bytebase让你爱上tidb的开源审核神器。

TiDB 社区干货传送门

6.x 实践

基于TiDB Binlog架构的主备集群部署及数据同步操作手册

TiDB 社区干货传送门

管理与运维

下游需求趋势长期向好,高端产品国产替代空间广阔

华秋电子

ElasticSearch 拼音搜索自定义扩展插件(长拼音序列)

alexgaoyh

中文分词 分词 Elastic Search 自定义插件

都想成为架构师,那架构师需要掌握哪些知识体系呢?

做梦都在改BUG

基于TiDB+Flink实现的滑动窗口实时累计指标算法

TiDB 社区干货传送门

应用适配 HTAP 场景实践 大数据场景实践 实时数仓场景实践 OLTP 场景实践

〖产品思维训练白宝书 - 认知篇①〗- 产品思维能够为我们带来多大的价值?

哈哥撩编程

产品经理 产品思维

HummerRisk 使用教程: 多云检测

HummerCloud

云安全

构建云边端一体的分布式云架构,软硬结合驱动边缘计算创新场景

百度Geek说

人工智能 架构 分布式 边缘计算 企业号 3 月 PK 榜

2023年最新美团、字节、阿里、腾讯 Java 面经,已拿 offer(附面经分享)

采菊东篱下

Java 面试

龙蜥 Node.js/WebAssembly SIG 重磅发布 Node.js/Noslate 性能优化白皮书

OpenAnolis小助手

node.js Web 白皮书 龙蜥社区 sig

TiDB 数据库大版本升级-基于TiCDC异机升级

TiDB 社区干货传送门

迁移 版本升级

集群3副本丢失2副本-unsafe-recover

TiDB 社区干货传送门

实践案例 管理与运维 6.x 实践

熹微~~~基于Vue开发的昏暗风格的响应式网页!

京茶吉鹿

前端 项目 vue cli

【3.24-3.31】写作社区优秀技术博文一览

InfoQ写作社区官方

热门活动 优质创作周报

用友BIP智能财务,助力企业构建世界一流预算管理体系

用友BIP

全面预算

Hologres技术揭秘:JSON半结构化数据的极致分析性能

阿里技术

json 半结构化数据

利用自动化平台可以做的那亿点事 |得物技术

得物技术

自动化

TiDB × 阿里云试用体验(随迟但到)

TiDB 社区干货传送门

版本测评

数据丢失不用怕,火山引擎DataLeap 提供排查解决方案

字节跳动数据平台

大数据 数据治理 数据研发 企业号 3 月 PK 榜

火山引擎A/B测试产品——DataTester 私有化架构分享

字节跳动数据平台

私有化部署 ab测试 A/B 测试 企业号 3 月 PK 榜

TiDB Operator常见问题和解决步骤(二)

TiDB 社区干货传送门

故障排查/诊断

DTALK直播预约 | 数据资产管理:金融机构数据价值释放的必经之路

袋鼠云数栈

数据资产管理

微服务架构中的链路超时分析

做梦都在改BUG

Java 架构 微服务

软件测试/测试开发丨移动端App自动化之App控件定位

测试人

软件测试 自动化测试 测试开发

数据擘画资产全景 AI诊断故障真因

用友BIP

软件测试/测试开发丨利用 pytest 玩转数据驱动测试框架

测试人

软件测试 自动化测试 测试开发 pytest

和细胞一样优雅的 TiDB Region 设计

TiDB 社区干货传送门

TiDB 底层架构

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