前端做自动化测试 —— 用TDD的思想做组件测试

article/2025/10/11 18:40:01

Test-Driven Development(测试驱动开发,以下简述TDD)是一种设计方法论, 原理是在开发功能代码之前, 先编写单元测试用例代码, 通过测试来推动整个开发的进行.

本文详细描述了在创作 react-stillness-component,

 组件的过程中, 是如何学习 TDD 的思想来完成功能开发以及测试的

一.前言-自动化测试相关

本篇文章是针对编写react组件过程中所产生的质量与功能保障的角度去写的,由于涉及到部分的名词,可能需要提前有部分的自动化测试相关知识;

本文的侧重点主要是在如何设计以及为何要使用 tdd 的方式来做组件测试,有任何的问题,也欢迎与作者进行讨论 😁

二.我所认为的 tdd 实际落地流程

首先简单回顾一下 tdd:

对应的实际行为可能是(来源于 wiki)

  1. 添加一个测试用例
  2. 运行该测试用例,得到失败的结果(因为还没有实现任何功能)
  3. 编写刚好能通过的最简单的代码
  4. 重新运行该测试用例,得到成功的结果
  5. 根据需要进行重构,并保证每次重构之后都能通过之前的测试用例
  6. 重复这个步骤,直至完成整个开发

当然,在实际的开发过程中,作者也针对现实情况做了一定的改造,先看下改造之后的流程:

这里主要是针对前端组件场景增加了一些比较重要的步骤

  • 确认用户场景,在什么样的情况下才考虑用到这个组件?包括了普通用户和专业用户涉及到的场景.需要考虑到涉及到 UI 框架的场景
  • 确认用户行为,也就是用户的具体操作是什么?可以先从自身的角度出发,再进行实际调研,观察类似的组件是如何被使用的
  • 确认用户环境,这里的环境包括了现代浏览器环境以及框架自身所处的开发环境.

在每次完成测试用例编写之前先是确认环节,保证功能不偏离初心;在每次测试完之后,再进行验证,而验证的手段可以是 BDD(后文会提到)也可以结合现实线上例子进行结合考虑,如果可以解决实际问题,则证明功能已完成.

当然,由于作者本身的日常习惯是先列计划 😂,这次也不例外:

可以看到最下面的部分就是与 TDD 相关的测试用例规划,实际编写用例过程中会有额外的情况,因此第二个部分也就是 e2e 模拟测试就是框定的使用范围,第一期只要不超出即可.

下面来看实际案例

三.实际案例

本文中使用的测试框架为**jest**,相关配置可参考问题汇总的第一点

provider

首先从最外层开始,该组件内部大量使用了context,因此,需要提供一个全局的provider,因为providervalue来源于createStillnessManager(),所以我们的第一个例子就是判断当提供了这个方法的时候,provider是否可以正常运行

it('Verify that the StillnessManager is correct', () => {let capturedManager;let manager = createStillnessManager(); // let mockManager: any = jest.fn();render(<StillnessContext.Provider value={{ stillnessManager: manager }}><StillnessContext.Consumer>{({ stillnessManager }) => {capturedManager = stillnessManager;return null;}}</StillnessContext.Consumer></StillnessContext.Provider>);expect(capturedManager).toBe(manager);
});
复制代码

这里前期使用的其实是jest mock functions,当createStillnessManager()编写完成之后,才被替换成真实的函数,两者的区别在于mock的方式可以过滤掉provider编写代码时的干扰项

那我们现在就可以开始 run test 了,当然,这时由于代码已编写完毕,无论是使用真实的参数还是模拟的参数都可以得到成功的例子.

而在一开始,未编写代码时,就可以遵循流程,编写真实代码了.

provider除了初始化之外,当然还会有其他功能,比如:

  • 卸载时会自动清除全局的缓存对象
  • 预防多个 provider 嵌套产生的错误,需要主动提醒用户

而针对这两点,我们就可以继续写测试用例了

it('stores StillnessManager in global context and cleans up on unmount', () => {let capturedManager;const { container, unmount } = render(<StillnessProvider><StillnessContext.Consumer>{({ stillnessManager }) => {capturedManager = stillnessManager;return null;}}</StillnessContext.Consumer></StillnessProvider>);const globalInstance = () => (global as any)[INSTANCE_SYM] as any;expect(globalInstance().stillnessManager).toEqual(capturedManager);unmount();expect(globalInstance()).toEqual(null);
});
复制代码

可以看到,这里通过调用返回的方法,从而达到了模拟卸载的效果.

class Component

首先来看下整个库的核心, <OffscreeenComponent>,相比较经过HOC包装之后的组件,原始组件的 props 就要复杂的多了

  • uniqueId: UniqueId;
  • parentId: UniqueId;
  • parentIsStillness: boolean;
  • isStillness: boolean;
  • stillnessManager: StillnessManager;

测试用例也围绕这几点即可,举个例子:

it('Does it prompt an error message when there is no context?', () => {global.console.error = jest.fn();expect(() => {render(<OffscreenComponentvisible={true}isStillness={false}uniqueId="test1"parentId={rootId}parentIsStillness={false}><div /></OffscreenComponent>);}).toThrow(/stillnessManager is required/i);
});
复制代码

组件是没办法在缺少 context 的情况下运行的,那我们在编写例子的时候只要排除这个参数就行,如果组件捕获了异常并抛出了,则说明功能是 ok 的,这个属于较为简单的例子

来看一个更加复杂的:

it('When the passed isStillness changes, clear the corresponding dom element or reload the original one', async () => {const Demo = ({ isStillness }: any) => {return (<OffscreenComponentvisible={true}isStillness={isStillness}uniqueId="test1"stillnessManager={mockStillnessManager()}parentId={rootId}parentIsStillness={false}><div data-testid="content" /></OffscreenComponent>);};const { queryByTestId, rerender } = render(<Demo isStillness={false} />);rerender(<Demo isStillness={true} />);expect(queryByTestId('content')).not.toBeInTheDocument();rerender(<Demo isStillness={false} />);expect(queryByTestId('content')).toBeInTheDocument();
});
复制代码

组件的isStillness属性是比较重要的,也是用来控制组件的静止与否的条件,在这里通过真实模拟render,并通过修改传参的方法,来直接模拟效果,如果传递了true,则组件应该会渲染在 body 中,也就是查找idcontent的元素一定可以找到,反之就找不到.

通过这种方法,就可以测试 class Component

更多例子请参考 Offscreen.spec.tsx

HOC

HOC是如何进行测试的呢,以<Offscreen>组件为例:

其 props 为:

  • visible:boolean 类型,控制组件是否静止
  • type:string or number,标识组件的类型,可重复,同一类型的静止行为会保持一致
  • scrollRest:boolean 类型,控制组件静止时是否缓存滚动位置

但这些props实际上是经过处理传递给<OffscreenComponent>组件的,

对于HOC自身来说,只需要保证在未找到context时进行捕获异常即可:

it('throw an error if rendered', () => {console.error = jest.fn();class TestClass extends React.Component<React.PropsWithChildren<OffscreenInnerProps>> {}const DecoratedClass = withNodeBridge(TestClass);expect(() => {render(<DecoratedClass visible />);}).toThrow(/Expected stillness component context/);
});
复制代码

至于上面的props,由于涉及到了其他模块,属于BDD测试的范畴了,会在下一篇 BDD 测试相关进行介绍

hooks

针对hooks相关,需要用到 @testing-library/react-hooks 该库可以直接运行 hooks 并断言结果

举例说明:

现在有一个根据依赖项从而返回最新结果的 hooks useOptionalFactory

代码为:

function useOptionalFactory<T>(arg: FactoryOrInstance<T>,deps?: unknown[]
): T {const memoDeps = [...(deps || [])];if (deps == null && typeof arg !== 'function') {memoDeps.push(arg);}return useMemo<T>(() => {return typeof arg === 'function' ? (arg as () => T)() : (arg as T);}, memoDeps);
}
复制代码

测试用例的代码为:

import { renderHook, act } from '@testing-library/react-hooks';const useTest = () => {const [count, setCount] = React.useState(0);const addCount = () => {setCount(count + 1);};const optionFactoryFn = useOptionalFactory(() => ({collect: () => {return {};},}),[count]);return { addCount, optionFactoryFn };
};describe('useOptionalFactory', () => {let hook;it('Depending on the variation of the dependency value, different results are generated', () => {act(() => {hook = renderHook(() => useTest());});let memoValue = hook.result.current.optionFactoryFn;act(() => {hook.result.current.addCount();});expect(memoValue).not.toStrictEqual(hook.result.current.optionFactoryFn);});
});
复制代码

通过使用 renderHooks() 与 act(),即可简单进行测试,当测试的依赖项变化时,返回值则跟随进行变化.

四.问题汇总

  1. 如何搭建测试环境?

    整体架构为lerna+Typescript+React+rollup+Jest,其实社区也有了很多的实例了,这里只介绍搭建过程中遇到的问题,

    • 如何单独搭建子包的测试环境? lerna 的架构,很好的分离了每个包的环境,可以使用不同的测试框架在每个子包中,单独配置,举例:

      可以在每个包中配置不同的 jest.config

    • 测试代码也希望使用Typescript?

      // jest-transformer.js
      const babelJest = require('babel-jest');module.exports = babelJest.createTransformer({presets: [['@babel/preset-env',{targets: {node: 'current',esmodules: true,},bugfixes: true,loose: true,},],'@babel/preset-typescript',],plugins: [['@babel/plugin-proposal-class-properties', { loose: true }],'@babel/plugin-transform-react-jsx',['@babel/plugin-proposal-private-methods', { loose: true }],['@babel/plugin-proposal-private-property-in-object',{ loose: true },],'@babel/plugin-proposal-object-rest-spread','@babel/plugin-transform-runtime',],
      });//jest.config.js
      module.exports = {setupFilesAfterEnv: ['./jest-setup.ts'],testMatch: ["**/__tests__/**/?(*.)(spec|test).[jt]s?(x)"],// testRegex: 'decorateHandler.spec.tsx',transform: {"\\.[jt]sx?$": "./jest-transformer.js",},collectCoverageFrom: ['**/src/**/*.tsx','**/src/**/*.ts','!**/__tests__/**','!**/dist/**',],globals: {__DEV__: true,},
      };
      复制代码

      只需要增加transform配置即可

  2. 如何测试实际的渲染效果?

    可使用 @testing-library/jest-dom,该库提供了关于 DOM 状态的相关 jest 匹配器,可用来检查元素的树形,文本,样式等,本文也介绍了一些,比如:

    • toBeInTheDocument:判断文档中是否存在元素
    • toHaveClass:判断给定元素中是否在其class属性中具有相应的类名
    • toBeVisible:判断给定元素是否对用户可见
  3. 想要单独测试某一个例子怎么办?

    //jest.config.js
    module.exports = {setupFilesAfterEnv: ['./jest-setup.ts'],//testMatch: ["**/__tests__/**/?(*.)(spec|test).[jt]s?(x)"],testRegex: 'decorateHandler.spec.tsx',transform: {"\\.[jt]sx?$": "./jest-transformer.js",},collectCoverageFrom: ['**/src/**/*.tsx','**/src/**/*.ts','!**/__tests__/**','!**/dist/**',],globals: {__DEV__: true,},
    };
    复制代码
  4. 如何自动测试?

    组件库中的自动流程体现在推送分支和github的自动发版流程上

    // package.json
    "scripts": {"test": "jest --projects ./packages/*/","test:coverage": "jest --coverage --projects ./packages/*/","precommit": "lint-staged","release": "bash ./scripts/release.sh","lint:staged": "lint-staged","ci": "run-s test:coverage vs git-is-clean",
    },
    "lint-staged": {"*./packages/**/*.{js,ts,json,css,less,md}": ["prettier --write","yarn lint"],"*./packages/**/__tests__/**/?(*.)(spec|test).[jt]s?(x)": ["yarn test"]
    }
    复制代码

    可以简单的修改配置文件,使用testRegex针对某一个文件进行测试,当然,这里作者只是列出了自身认为比较简单的方法,如果有更简单的方法,欢迎提出👏👏

五.总结

本文总结了在编写一个 react 组件的过程中是如何思考以及组织测试代码的,当然,在实际的生产开发阶段,有一定的测试时间才是最宝贵的,也是 TDD 测试能推行的基础,如果说 TDD 测试保证了基础功能,那么 BDD 测试则扩展了使用场景;

按照代码比例来说,作者自身认为 TDD 占 70%,而 BDD 则占到剩下的 30%;

这里面是性价比的考量,毕竟日常工作中,需求的改动是很频繁的,这也就意味着组件可能会遇到各种不同的场景,而 TDD 测试用例大部分仍然可以保留,但 BDD 测试就不一定了.

这是 《前端如何做组件测试》的第一篇,如果有任何问题,欢迎讨论.


学习资源分享和交流讨论

最后感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走

​这些资料,对于在从事【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!凡事要趁早,特别是技术行业,一定要提升技术功底。希望对大家有所帮助…….


http://chatgpt.dhexx.cn/article/H0CpnXFv.shtml

相关文章

深度解读 - TDD(测试驱动开发)

本文结构&#xff1a; 什么是 TDD为什么要 TDD怎么 TDDFAQ学习路径延伸阅读 什么是 TDD TDD 有广义和狭义之分&#xff0c;常说的是狭义的 TDD&#xff0c;也就是 UTDD&#xff08;Unit Test Driven Development&#xff09;。广义的 TDD 是 ATDD&#xff08;Acceptance Tes…

【TDD】测试驱动开发

欢迎关注微信公众号“Python小灶&#xff0c;和我一起每天学习Python新知识”&#xff0c;还可添加博主Vx&#xff1a;yf03064131&#xff0c;方便一起交流学习。 或者B站搜索 有只小新 原视频地址链接&#xff1a;点击这里 代码地址&#xff1a;点击这里 本文为大致翻译以及…

测试驱动开发(TDD)的学习使用

测试驱动开发TDD 是一种不同于传统软件开发流程的新型开发方式 特点&#xff1a; 先编写测试代码 – 实现主要逻辑 再写功能代码 – 实现细节 通过测试来推动整个开发的进行。 有助于编写简洁可用和高质量的代码&#xff0c;并加速开发过程呢。 比如写一个Person类&#xff0c;…

从 TDD 到测试策略

“ 前端没法 TDD / 前端不容易做 TDD / 前端 TDD 收益不大 ” 这是进公司后无数人给我判的“死刑”。 事实上好像的确如此&#xff1f; 在这个崇尚敏捷的组织里&#xff0c;我们有毕业生的入职前培训&#xff0c;入职后培训&#xff0c;有 TwU&#xff0c;有无数定期不定期的…

使用ddt执行数据驱动测试

所谓数据驱动测试&#xff0c;简单的理解为数据的改变从而驱动自动化测试的执行&#xff0c;最终引起测试结果的改变。通过使用数据驱动测试的方法&#xff0c;可以在需要验证多组数据测试场景中&#xff0c;使用外部数据源实现对输入输出与期望值的参数化&#xff0c;避免在测…

先测试再开发?TDD测试驱动开发了解一下?

1、什么是TDD 我第一次接触TDD这个概念&#xff0c;是在<<代码整洁之道>>中&#xff0c;作者鲍勃大叔在书中&#xff0c;写了一些关于测试代码的代码规范&#xff0c;其实就提到了有关TDD三定律: - 定律一&#xff1a; 在编写不能通过的单元测试前&#xff0c;不…

测试驱动开发(TDD)前端篇

当你在写生产代码时&#xff0c;你处在高认知的状态&#xff08;obvious&#xff09;&#xff0c;你的研发流程和你的工程实践&#xff0c;有助于你一步一步的提升你的认知能力&#xff0c;把你的问题进行一个降解&#xff08;分解&#xff09;&#xff0c;只要你做到同样的事情…

TDD 开发测试

测试驱动开发(Test-Driven Development)。是敏捷开发中的一项核心实践和技术。 TDD是在开发功能代码之前&#xff0c;先编写单元测试用例代码&#xff0c;测试代码确定需要编写什么产品代码。 变红 ——> 变绿 ——> 重构 在进行 TDD 案例编写的时候&#xff0c;看一…

TDD测试驱动开发

TDD测试驱动开发 什么是测试驱动开发测试驱动开发该怎么做&#xff1f;需求一&#xff1a;输入一个非元音字符&#xff0c;并预期返回字符本身 (输入"h" 返回“h”)需求二&#xff1a;输入一个元音&#xff08;a,e,i,o,u&#xff09;&#xff0c;返回 mommy (输入&qu…

TDD测试驱动学习

gtest 和 gmock 安装 //如果不知道对应库名字可以执行这个命令查找对应库,如果没找到要去跟新对应的源sudo apt update sudo apt-cache search gtest sudo apt-cache search gmock 测试例子 #include <string> using std::string;// 定义 Soundex 类 class Soundex {pub…

TDD (test driver development)测试驱动开发

##为什么需要测试驱动/或者说需要单元测试 我们工作接触的软件项目&#xff0c;不是学生时代&#xff0c;玩一玩就不管了&#xff0c;工作的项目&#xff0c;需要长期维护&#xff0c;并且随着时间的推移需要增加新的需求&#xff0c;进行修改&#xff0c;优化。此时已经距离你…

tdd(测试驱动开发)的概述

最近的工作的项目&#xff0c;使用了tdd&#xff08;test-driven development测试驱动开发&#xff09;的开发模式。 这两几年大概听说了无数种xxx-dd, ddd, tdd, atdd, bdd, fdd, udd各种名词眼花缭乱&#xff0c;当然很多dd其实也有相互借鉴&#xff08;抄袭&#xff09;的部…

测试驱动开发(TDD)实践与技巧

文章目录 引言Google Mock测试用例结构断言经典式断言Hamcrest 断言 测试驱动开发&#xff1a;第一个示例开场白开始吧去掉不干净的代码增量性fixture 设置思索与测试驱动开发测试驱动与测试 测试驱动开发基础与单元测试单元测试的组织结构测试驱动开发周期&#xff1a;红-绿-重…

opencv配置相关的截图参考

opencv配置相关的截图参考&#xff0c;如下&#xff1a;

Anaconda3安装及opencv配置

一、Anaconda安装 1.直接百度搜anaconda&#xff0c;进入官网即可&#xff08;anaconda网站链接&#xff09; 2.点击图片上黑框&#xff08;Get Started&#xff09;即可进入下一步&#xff0c;选择下图中第四个 3.选择适合电脑类型的anaconda安装器&#xff0c;注意选好64位…

linux安装配置opencv

刚开始学习ubuntu&#xff0c;有些项目需要用到opencv&#xff0c;当我用下面的命令安装包的时候&#xff0c;总是出现“E&#xff1a;无法定位软件包 opencv”的错误。然后开始着手解决&#xff0c;网上搜集了很多的教程&#xff0c;大部分都是说要更换源&#xff0c;我也照做…

opencv安装配置测试

前面安装了pcl和qt&#xff0c;以及qt中的vtk&#xff0c;这里配置下opencv4.3 将cv复制到D盘下。 安装完成之后&#xff0c;添加环境变量&#xff0c;[计算机]->右键 [属性]->[高级系统设置]->[环境变量]->[系统环境变量]->编辑 [Path]&#xff0c;添加“D:…

Qt中配置OpenCV

Qt中配置OpenCV 1. 环境下载2. 进行编译和安装2.1 新建opencv-build文件夹&#xff0c;用于opencv的 编译和安装&#xff08;直接在opencv下新建即可&#xff09;2.2 CMake设置2.3 命令行进行编译安装 3. Qt测试4. 其它问题4.1 若电脑上安装有PyQt4.2 电脑无法访问wai网 1. 环境…

Anaconda配置OpenCV

文章目录 1.安装Anaconda2.配置OpenCV2.1打开Anaconda Prompt2.2找到里面Scripts的路径2.3下载2.4验证是否配置成功 3.出错解决办法4.参考文章 1.安装Anaconda 可以查看我的上一篇文章&#xff1a;Anaconda下载、安装和环境配置 2.配置OpenCV 2.1打开Anaconda Prompt 在开始…

Opencv学习笔记——opencv配置安装与IDE环境安装

文章目录 前言一、opencv配置安装二、IDE的安装总结 前言 既然开始对AI视觉这个方面有兴趣&#xff0c;也初步接触了一些AI视觉在嵌入式方面的应用&#xff0c;那自然少不了对Opencv的学习。到现在开始学习opencv之前对它了解不多&#xff0c;只知道opencv的全称是Open Source…