阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

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

评论 1 条评论

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

自主AI代理:未来的生产力引擎

互联网工科生

人工智能 AI代理

火山引擎DataLeap的Data Catalog系统公有云实践 (上)

字节跳动数据平台

大数据 数据中台 数据治理 数据安全 企业号 7 月 PK 榜

Coral Finance 将为 Zepoch 节点空投,Nautilus生态空投季开启

西柚子

万字长文浅析配置对MySQL服务器的影响 | 京东物流技术团队

京东科技开发者

MySQL 数据库 服务器 企业号 7 月 PK 榜 MySQL服务器

全彩LED显示屏品质由什么决定

Dylan

媒体 广告 科技 LED LED显示屏

中文人物关系知识图谱(含码源):中文人物关系图谱构建、数据回标、基于远程监督人物关系抽取、知识问答等应用.

汀丶人工智能

人工智能 nlp 知识图谱 智能问答

广东省《5A物理抗菌纺织品》团体标准颁布

Geek_2d6073

Sugar BI:大模型时代的智能 BI

Baidu AICLOUD

BI 数据智能

618技术揭秘 - 大促弹窗搭投实践 | 京东云技术团队

京东科技开发者

前端 弹窗 xview 企业号 7 月 PK 榜

KaiwuDB 资深解决方案专家周幸骏:打造核心时序引擎,释放数据新价值

KaiwuDB

时序数据 KaiwuDB

高性能、高扩展、高稳定:解读 EasyMR 大数据组件自定义可扩展能力

袋鼠云数栈

大数据 大数据组件

REST API 版本控制:高效管理

Apifox

程序员 RESTful API REST API API 测试

基于因果关系知识库的因果事件图谱构建

汀丶人工智能

人工智能 自然语言处理 知识图谱

源码解析Collections.sort ——从一个逃过单测的 bug 说起 | 京东云技术团队

京东科技开发者

排序算法 源码解读 企业号 7 月 PK 榜 Collections.sort

支付宝小程序云李铮:科技赋能,敏捷增长

TRaaS

支付宝小程序 小程序云开放 蚂蚁

一种配置化的数据脱敏与反脱敏框架实现 | 京东云技术团队

京东科技开发者

数据安全 脱敏 数据脱敏 企业号 7 月 PK 榜

免费SAFe大规模敏捷管理工具

顿顿顿

Scrum #敏捷开发 敏捷开发管理工具 SAFe框架

内部Java内存模型与硬件层面内存模型的关系是什么?

java易二三

Java 编程 程序员 硬件 计算机

飞桨大模型分布式训练技术

Baidu AICLOUD

飞桨 百度百舸 AI 大底座

我身边IT业40岁的老家伙们都去哪儿了?

高端章鱼哥

程序员 IT 大牛

从分片传输到并行传输之大文件传输加速技术

镭速

大文件传输

IPQ9574 VS IPQ6010 VS IPQ5018 VS IPQ4019-Understand the performance of different chips

wifi6-yiyi

wifi WiFi7 wifi5,wifi6

详解!视频直播源码布谷科技平台搭建开发:录制功能

山东布谷科技

软件开发 视频直播 源码搭建 短视频直播源码 视频录制

明晚直播:可重构计算芯片的AI创新应用分享!

飞桨PaddlePaddle

人工智能 百度 paddle 飞桨 百度飞桨

科研类项目核算的“法、术、器”(二)

用友BIP

项目管理 科研项目

基于Hologres向量计算与大模型免费定制专属聊天机器人

阿里云大数据AI技术

人工智能 hologres

Java基础之IO流

java易二三

Java 编程 程序员 io 计算机

浅聊一下大模型

鲸品堂

大模型训练 大模型

2023开源数据库排行榜发布,“新晋黑马”瀚高IvorySQL跻身三十强

Geek_2d6073

Coral Finance 将为 Zepoch 节点空投,Nautilus生态空投季开启

大瞿科技

超强阵容!HarmonyOS极客马拉松2023专家评审团来袭!

HarmonyOS开发者

HarmonyOS

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