写点什么

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

评论 1 条评论

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

甲方日常 47

句子

工作 随笔杂谈 日常

2020双11:每秒58.3万笔!阿里云又扛住了!

云计算 互联网 运维 云原生 科技

一个技术总监的忠告:精通那么多技术,你为何还是受不到重用?

四猿外

程序人生 技术管理 加薪 职场成长 源码阅读

Spring bean 加载顺序导致的 bug 问题

AI乔治

Java 架构 Spring Boot

双11购物节国外剁手党同狂欢 阿里云视频云电商直播实时字幕

阿里云CloudImagine

云直播 直播 直播带货 语音识别

什么?美团T9首发内部JVM高级特性笔记,看完差距不止一点

小Q

Java 学习 程序员 架构 面试

如何预防工业物联网中的恶意攻击?

VoltDB

大数据 数据分析 5G 工业互联网

当我们在讨论实时性的时候,我们在讨论什么?

VoltDB

数据分析 5G 工业互联网

文科妹子都会用 GitHub,你这个工科生还等什么

沉默王二

GitHub

Rethink:多版本文件的命名细节

小匚

团队 随笔杂谈

Reactor中的Thread和Scheduler

程序那些事

响应式编程 reactor 多线程 程序那些事 reactivex

实时指挥调度的发展和优势

anyRTC开发者

ios android 音视频 WebRTC RTC

低代码开发平台核心功能设计——组件自定义交互实现

徐小夕

大前端 编辑器 H5 大屏可视化 lowcode

Pulsar Summit Asia 2020 | 主题演讲:大咖呈现,紧扣社区

Apache Pulsar

大数据 开源

接口文档生成详细教程

测试人生路

接口文档

数据结构与算法系列之递归(GO)

书旅

数据结构与算法 Go 语言

O'Reilly出版社又一经典之作——Python设计模式

计算机与AI

Python

如何应对大促流量洪峰?揭秘京东技术人的备战手册

京东科技开发者

云计算 大数据 亿级流量

微信视频号强制置顶朋友圈:盈利不可牺牲用户体验

石头IT视角

甲方日常 48

句子

工作 随笔杂谈 日常

代码简易调试方法.md

Albert

Java LeetCode 调试

祝贺 StreamNative 团队成员 Jennifer 当选 Apache Pulsar PMC 成员

Apache Pulsar

大数据 开源 Apache Pulsar

架构师训练营第 1 期第 8 周学习总结

好吃不贵

极客大学架构师训练营

记不住Spring中Scheduled中的Cron语法?让我们看看源码吧

AI乔治

Java spring 编程 架构

Dubbo-go Client端调用服务过程

apache/dubbo-go

dubbo dubbo-go dubbogo

靠脑机接口“隔空探物”,大脑植入芯片可实现“心灵感应”

脑极体

当人脸识别对准执法者,AI的应用边界博弈

脑极体

HTTPDNS开源 Android SDK,赋能更多开发者参与共建

移动研发平台EMAS

android 阿里云 开源 httpdns 移动研发平台

这份笔记我必啃完!美团T9首发内部JVM高级特性笔记,差距不止一点点

Java架构追梦

Java 源码 架构 面试 JVM

架构师训练营第八周

我是谁

极客大学架构师训练营

2 w字长文带你深入理解线程池

Java架构师迁哥

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