-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React 单元测试策略及落地 #200
Comments
牛逼到我没有语言,只能66666666 |
两会的测试 session 有一个点我觉得很棒,说「策略本身的 effort 会决定策略被落地的可能性」,大概是这个意思。我感觉也是这样,但这个事情背后的假设是悲观的…就是复杂的东西没人会用,哪怕是好的? |
啊呜,之后项目都要用Vue了,🤣 |
意思是 React 太难没人学吗🤣 |
只是为了统一技术栈而已,我觉得也行吧,只是Vue的单元测试又需要研究一下了… |
好在这篇文章直到「好测试的特征」之前的部分都是能复用的🤣 |
把后面的落地一下,明天出篇 Vue 测试策略的文章没问题吧… |
哈哈哈哈,🤣 组合型写作,JimmyLv/jimmylv.github.io#250 |
哈哈哈哈,组件型写作。general ideas 多个地方都可以用,落地的具体实践部分,以几个具体原则做指导(相当于接口),再填充具体的东西就行…大家求变,要抓住不变! export Blog = () => (
<GeneralIdeas />
<Practice />
)
interface Practice() {
whatMakesAGoodTestCase() {}
whatMakesAGoodTest() {}
testingStrategy() {}
}
export ReactInPractice extends Practice { ... }
export VueInPractice extends Practice { ... } |
给一些我的反馈哈。总体来说,我觉得文章写的很好,很落地,对 react 和 redux 前端开发做 UT 给出了非常详细的指导和建议,让我学到很多。我最近做 RN 的开发,写的前端代码还不多,需要根据本文中的建议去实践一下。 |
单元测试的上下文,测试策略和好测试的特征这几节绝大部分的内容,我很认同,我也是 TDD 的粉丝哈。 |
reducer 的测试部分,我很认同。reducer应该被测试,但是没必要通过创建store来测试,直接纯函数测试就好了。不过,我对单元测试的“表达力”要求比较高,所以我会建议重构一下那个merge的UT。大致如下:
主要就是把测试只体现关键点(这里应该是 merge相关的数据),其他不相关的数据应该不出现。之前我提到“好测试的特征”那一节少了一点,就是“测试的表现力”,这个对测试的理解和维护至关重要。 |
本来想等你回完再 一起回,忍不住,由于提交比较简短我就合并一下好了。 「端到端测试维护成本高,运行时间长、速度慢」这个「常见的认知」,确实是我没有实践清楚就套用定势认知的部分,感谢澄清,十分有益。嗯,如果说验收测试可以做到和 UT 一样级别的维护成本,那是个很有意义的方向,十分期待了解更多的实践和总结。你这篇文章好像是讲持续集成和 git 的?我也看过,哈哈。 这个表达力的反馈赞,这就修改下。确实就精简测试数据本身,都能减少噪音,提高表达力。这让我想起,有时测试数据太多,我们还会编写一些 fixture 类的东西来帮忙组织、提高表达力,应该也是类似的作用。然后这些 fixture、testing helper,上面讲的专为测试写的 |
先赞一下你的行动力哈。之前那个关于验收测试的链接给错了。。。我已经在那个commit改好了,欢迎交流。 继续我对文章的反馈哈。
有个问题想交流一下,你们项目中有写过 middleware 吗?有的话,UT 又是如何实现的呢?我自己写过 middleware 及其对应的 UT,感觉 UT 还是必须的 |
对于 react component 的 UT,我个人感觉是�本文中写的最深入的部分了。我之前对于 �component 的 UT 有不少困惑的地方,看完基本都有思路了,后面一定会找机会来实践的。 就是有个小问题想讨论一下。�文中有个叫 ProductDetailPage 的 component,代码如下:
我的问题是,这行代码
也就是把检查 length 的逻辑移到 Comments 这个 component 里面去。原来的代码有点“特性嫉妒”的臭味,就是 ProductDetailPage 替 Comments 做了检查,然而数据则是属于 Comments 的。 这样重构之后,就会发现 ProductDetailPage 成了下面这样,我觉得对他做 UT 就没有必要了。原因是他实际上只是做了数据传递的事情,没有其他逻辑了。
我的反馈到这里也就结束了。最后,再次感谢你写了如此精彩的一篇前端 UT 的文章。 |
selector 这个东西,我们项目实践了下觉得是个不错的东西,如果用上可以多交流。 middleware 的话,我们的 middleware 比较简单,是个 API interceptor,大概长这样: import axios from 'axios'
export default function ({ getStore, dispatch }) {
axios.interceptor.request.use(
config => addHeadersForSpecifiedRequest(config),
error => error
)
return (next) => (action) => next(action)
} 这个场景下, 如果有更复杂的 middleware 测试需求,不妨贴出来分享下。另外是官方文档也有一部分提及 middleware 及其测试的,也可参考参考:https://redux.js.org/recipes/writing-tests#middleware 。我看文档的做法基本就是 mock 掉 |
component 那个很有道理,我重构基础又没打扎实😂平凡之处见功夫哈。这个例子我可以找个其他更恰当的,顺便重构手法 get,这个地方确实是 feature envy 了,判空这个行为应该是属于 另外 components 部分我还打算补充更多的例子,比如有时可能会需要用到 mock 的,还有一些其他的例子,不过大的方向跟我们做 UT 遵循的原则是一致的。 十分感谢这份详尽的反馈,阅读+反馈也是需要很多精力的,刚好遇到对主题有相似思考又有实践需求/经验的读者,还愿意花时间给予详尽的反馈,感觉特别好运,哈哈。 互相吹捧完毕。。 |
有个小 tips 不晓得你知不知道: 写 markdown 的时候,代码片段可以指定语言,这样渲染出来的 markdown 会有对应的语法高亮效果,比如: ```js const React = vue({ id: 1 }) ``` 会被渲染为: const React = vue({ id: 1 }) |
很感谢你的回复哈。上面的那个小 tips,学习了。 难得有文章可以让我这么投入的回复,以后多交流哈。🙂 |
很久没有看到这样的好文章了,把React单元测试分析的比较全面,思路非常清楚,对单元测试的理解非常到位。有几点反馈:除了 @JosephYao 提到的测试表现力外,测试数据的准备有时不需要写的冗长包含各种细节,只突出关键。比如: const comments = [
{ id: 283992, author: '男***8', comment: '价廉物美,相信奥康旗舰店' },
{ id: 283993, author: '雨***成', comment: '因为工作穿皮鞋时间较多,所以一双合脚的鞋子...' },
{ id: 283994, author: '叶***亮', comment: '不错,很软,我买了大一码,因为脚宽些,是真皮的,划算萌东上...' },
{ id: 283995, author: '替***崽', comment: '磕到了,脚踝疼得不好穿,要不你们试试' },
] 我的理解是这里comments的内容对测试是完全无意义的,可以直接提取成数据方法comments(),后面也有测试也用到comments数据,也是一样数据内容本身无意义,这些都可以复用的。这样写你会发现慢慢你有了自己的测试数据构建的一套方法,可以形成自己的DSL,只在测试中暴露必要的相关信息。 |
另外,对我来说测试要明确测试的意图,也就是测试的目标逻辑要清晰。其实你讲的很多内容都是在达成这一点,比如仅对输入输出敏感、不依赖于内部实现,以及什么测什么不测,你举的很多例子都很好的说明了这点。但是在Saga测试这一段,我觉得没有说清楚(当然也有可能是我Saga用的少)。 我觉得这里前后的两个例子并没有很好的证明你的观点“仅对输入输出敏感、不依赖于内部实现”。如果按照“正确的姿势”来写前一个例子,其实真正让你觉得舒服,符合你描述的有点,关键在于toHaveBeenDispatched这个matcher。 为什么这么说呢?“不完美的姿势”让你觉得“违反上述所说「仅对输入输出敏感」的原则”,或者改动代码容易让测试失败的原因是使用了Generator。Generator返回的迭代器是关切顺序的。这里实际代码的要实现的逻辑,据我理解是不关心顺序的,我只要取了这些数据就行。 而你使用testSaga和toHaveBeenDispatched恰恰就避免了顺序的影响。因此,如果“不完美的姿势”里,写一个工具方法把Generator返回的迭代器里的内容放到一个数组或hash对象之类的容器里,只需判断容器内是否有你想要的action就行。 而stub select函数或者api调用,我觉得差别其实没多大。 总体而言,我觉得用testSaga来测试也没什么问题,但就saga的输入输出而言,确实就是参数和迭代器,因为saga就是个Generator函数而已,虽然它的唯一使用者是redux-saga。不用testSaga也可以实现你要的效果,所以在这里这两个例子没有充分表述你的观点。 我只是读你这段的时候稍微花了点时间研究了Saga和Generator,所以如果我的理解有问题,你也可以帮忙指出。谢谢😄 |
哦,对了,为了明确测试的意图,我通常不用参数化测试,因为很容易迷失掉测试的意图(对于写和读测试的人都一样),纯粹覆盖可能的数据。 |
测试意图和表达力在你的回复中,我感受到了对测试意图和表现力的极致追求,看来我的「偏执」还有很多提升空间嘛。 「只在测试中暴露必须的信息」这点很有道理,补充了我之前不太留意的一个视角(原来知道但没去做得更好)。确实,有时看到长长的测试,很容易产生「这很复杂」的感觉,进而降低阅读效率和体验,无法一眼了解测试的意图。 saga 测试相关关于 saga 的那段是个很好的反馈,我觉得你的理解没问题,我这个地方例子举得确实没表达到我要表达的东西。原来「不完美的姿势」一节,我想表达最大的不完美的点是在于顺序对测试的影响。所以:
是,这其实就是我最想表达的点,也是 感觉我前后用了两个不同的例子,使得比较效果模糊了。如果我把例子改成这样,你觉得对于表达「原来测试与次序耦合,是痛点;改良后的测试避免了顺序的影响,让你真正专注于测试场景,并很好支持了重构」这个观点是否会有帮助: export function* someSaga() {
yield put(actions.notImportantAction1())
yield put(actions.notImportantAction2())
yield put(actions.notImportantAction3())
yield put(actions.notImportantAction4())
const recommendations = yield call(Api.get, 'products/recommended')
const products = recommendations.length > 5 ? getFirstFiveProducts(recommendations) : recommendations
yield put(actions.importantActionToSaveRecommendedProducts(products))
} 依赖于次序的写法是这样: test('should only save first five recommended products when there are more than 5 recommendations', () => {
const sixRecommendedProducts = [product(1), product(2), product(3), product(4), product(5), product(6)]
const firstFiveProducts = [product(1), product(2), product(3), product(4), product(5)]
const generator = cloneableGenerator(someSaga)()
expect(generator.next().value()).toEqual(put(actions.notImportantAction1()))
expect(generator.next().value()).toEqual(put(actions.notImportantAction2()))
expect(generator.next().value()).toEqual(put(actions.notImportantAction3()))
expect(generator.next().value()).toEqual(put(actions.notImportantAction4()))
expect(generator.next().value()).toEqual(call(Api,get, 'products/recommended'))
expect(generator.next(sixRecommendedProducts).value()).toEqual(put(actions.importantActionToSaveRecommendedProducts(firstFiveProducts)))
}) 而有了 test('should only save first five recommended products when there are more than 5 recommendations', async () => {
const sixRecommendedProducts = [product(1), product(2), product(3), product(4), product(5), product(6)]
const firstFiveProducts = [product(1), product(2), product(3), product(4), product(5)]
Api.get = jest.fn().mockImplementations(() => sixRecommendedProducts)
await testSaga(someSaga)
expect(Api.get).toHaveBeenCalledWith('products/recommended')
expect(actions.importantActionToSaveRecommendedProducts).toHaveBeenDispatchedWith(firstFiveProducts)
}) 参数化测试这是个好点,就是说测试本身它有个等价类划分的过程,可能表现出来是 test.each([
[['0', '99'], 0.99, '(整数部分为0时也应返回)'],
[['5', '00'], 5, '(小数部分不足时应该补0)'],
[['5', '10'], 5.1],
[['4', '38'], 4.38],
[['4', '99'], 4.994, '(超过默认2位的小数的直接截断,不四舍五入)'],
[['4', '99'], 4.995],
[['4', '99'], 4.996],
[['-0', '50'], -0.5, '(整数部分为负数时应该保留负号)'],
])('should return %s when number is %s%s', (expected, input, description) => {
expect(truncateAndPadTrailingZeros(input)).toEqual(expected)
}) 当然,这个 API 也不够简洁了, |
这真是好非常不错的一点,清晰有用! 关于参数化测试,Jest 23.0 最新版有一种表达力更为优雅的方式,即不需要 each`
a | b | expected | description
${1} | ${1} | ${2} | xxxxx
${1} | ${2} | ${3} | xxxxxxx
${2} | ${1} | ${3} | xxxxxx
`.test('returns $expected when adding $a to $b', ({a, b, expected}) => {
expect(a + b).toBe(expected);
}); |
而且 Prettier 也很快就支持了表格的自动格式化,不过想来要手打这个 这个 |
少年简直可以哒!这样连 |
@garymabin 感谢反馈。关于什么是好测试这块的反馈,后面我会再重新组织下语言。async action creator 这块,我理解如何它是关于处理副作用的一种方案(比如 redux-thunk),那么确实是应该测试的,测试的关注点也跟以 saga 测试为例提到的类似。不过在 action 那节,也许可以增加以下对不同类型 action 的说明,确实就 action 类型不同是有不同策略的。 |
@azzgo 我们的Vue组件库在尝试用Image Snapshot Testing,光 HTML output 对比起来还是不够清晰,不便于肉眼区分:
效果就是这样: ref: |
@JimmyLv 但是这样做跑着很慢吧,毕竟是要跑个 浏览器。而且无法 HTML 虽然肉眼看不友好,但是对排差产出的数据是否符合要求方面,排错会更加容易些。不过也是个思路,我觉得可以尝试下。 我理解px级别对比,基本是对还原度,QA 业务测试会更有帮助,对开发测试而言,帮助较少 |
嗯,放到CI上让他跑就是了,还有现成的产品已经做出来了,感觉是门生意。 https://www.chromaticqa.com/static/website-workflow-test-every-component-optimized.mp4 对数据差异进行排错这一点很赞同。然后我个人感觉Snapshot Testing最烦的一点就是需要手动更新,凡是手动就会有出错,就会有懈怠;凡是手动确认,就是成本就要付出努力。比如说累了一天,写了很多代码,然后run一次SnapshotTesting竟然那么多diff,算了不管了直接选择update吧,我写的代码肯定没问题,肯定算是“好”的Snapshot。 然后就不管三七二十一更新了,那么产出的Snapshot根本就无法作为验收条件,反而是它的回归作用会更大一点,只有在重构的时候,不破坏数据输出,或者HTML结构的时候会比较有用。 |
@JimmyLv 所以要提高频率啊, husky 之类的,持续交付的精髓 |
这篇文章经过润色,已经吸收了上面提到的基本所有修改意见。现在基本是一篇可以阅读的成系统的文章了。本 issue 的文章也已同步更新,同时发布在了自己的博客和掘金上: |
这个应该是目前全网最详尽的React单元测试文章(中文) 但是我还有一些疑惑没在文章中找到答案 不可否认,前端的变动大,可测试的点也多(样式,交互,DOM结构),随之出现的问题就是测试成本高,测试代码脆弱 |
什么值得测这个问题,结合TDD的前提来看,我觉得是这样:随故事卡交付的功能点,其代码都应有良好的设计、良好的测试;不存在值得测的区别,因为理想情况下,我们不验收没有测试的代码。这样做的好处有两点:后续功能易于修改重构、TDD能部分提升开发过程的反馈速度。但实际上,由于你提到的“前端可测试点多”、团队TDD习惯与能力、团队开发流程、团队结构、许多团队无法开展重构等等现实原因,很少团队能够以这种理想流程进行开发,要么是现有代码库没测试,要么是不会,这才会有什么值得测的问题引出来。这其实是个团队特点、开发流程、业务方向等等方向的综合问题。 我在文中提到,设计良好的单元测试策略、写好的单元测试,主要是为了提高团队响应力和支撑重构,但现实是,只靠单一的单元测试策略,没有其他设施的配合,也无从完成这两个目标。举个例子,响应力的提高同时还需要更敏捷的迭代、更全功能的团队、具备随时重构的能力;而重构的发生需要主干开发、持续集成、全功能团队而非部门墙、代码所有制而非私有制等等契机。所以,不重构、靠手工测试、靠测试部门保障质量是行业现状,如果项目仅仅希望通过提高覆盖率、策略性测试代码的方式来提升质量,而没有同时提升其他方面,往往很难成功。 至于前端重构,又有更多待解决的问题,比如,什么样的测试能够真正支持样式、DOM结构、交互的重构(改变实现但不改变功能,测试依然能通过)?所以,关于什么值得测的问题,似乎只能回答:理想情况下,没有测试不写代码;非理想情况下,只能尽量努力改善而没有简单方案。 |
刚接触jest测试,我现在有很多关于数据方面这块的清洗功能,我是放在utils工具类里面,因为放在组件里面太不好看了,所以当我去test utils.js时候,我有个问题,因为肯定要有入参,那么这些入参都得自己手动生产吗,还是从api里面获取,希望作者大大以及各位有经验的给个建议 |
@TYQbag 假设你要走测试utils这个方式,那么入参一般是需要你自己手动生产的,这个是测试三部曲里准备 “从api里面获取”指的是什么呢?可以把更详细的上下文信息(比如一般谁怎么用你这个util)贴出来,看看这个测试是不是可以有其他的方式写。 |
我这边是这样的,之前没要求做jest测试,然后现在交付有要求,所以基本是先完成的code,再回头来写jest测试,就是感觉,入参和expect都是自己手动生产的,有一种明显就知道是什么结果了,感觉就jest就是一次覆盖测试而已
…------------------ 原始邮件 ------------------
发件人: "Linesh"<notifications@github.com>;
发送时间: 2020年8月7日(星期五) 上午10:44
收件人: "linesh-simplicity/linesh-simplicity.github.io"<linesh-simplicity.github.io@noreply.github.com>;
抄送: "想"<1104295080@qq.com>; "Mention"<mention@noreply.github.com>;
主题: Re: [linesh-simplicity/linesh-simplicity.github.io] React 单元测试策略及落地 (#200)
@TYQbag 假设你要走测试utils这个方式,那么入参一般是需要你自己手动生产的,这个是测试三部曲里准备input的部分。
“从api里面获取”指的是什么呢?可以把更详细的上下文信息(比如一般谁怎么用你这个util)贴出来,看看这个测试是不是可以有其他的方式写。
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
@TYQbag 还是有个好处的,那就是如果日后不小心改动了功能,测试应该会挂掉提醒你。支持重构、作为回归套件是自动化测试的两个关键好处。 为了达到这个目标,你写的测试需要做到不去了解代码实现细节,只固定输入输出。如果做不到这点,未来稍微改点代码你就得改测试,重构点东西没改功能却也要改一堆测试,现在还要浪费时间写测试,三重浪费,其实就是坑自己+摸鱼,达不到测试应有的效果。 |
所以你的建议是?在code之前就把测试用例写好吗?我也觉得这样回头来写jest简直没啥用
…------------------ 原始邮件 ------------------
发件人: "Linesh"<notifications@github.com>;
发送时间: 2020年8月7日(星期五) 中午11:01
收件人: "linesh-simplicity/linesh-simplicity.github.io"<linesh-simplicity.github.io@noreply.github.com>;
抄送: "想"<1104295080@qq.com>; "Mention"<mention@noreply.github.com>;
主题: Re: [linesh-simplicity/linesh-simplicity.github.io] React 单元测试策略及落地 (#200)
@TYQbag 还是有个好处的,那就是如果日后不小心改动了功能,测试应该会挂掉提醒你。支持重构、作为回归套件是自动化测试的两个关键好处。
为了达到这个目标,你写的测试需要做到不去了解代码实现细节,只固定输入输出。如果做不到这点,未来稍微改点代码你就得改测试,重构点东西没改功能却也要改一堆测试,现在还要浪费时间写测试,三重浪费,其实就是坑自己+摸鱼,达不到测试应有的效果。
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
我觉得既然有成效的测试关键是支持重构和自动化回归,那么只要能符合这个标准的测试就是好测试。你是先写测试,或者后补测试,都可以写出这样的测试来。 怎么做呢?我觉得做法上,1)尝试按测试三步曲来写(准备数据输入、调用方法、断言输出);2)让测试只关注“输入”和“输出”,而不关注代码内部实现(避免大量mock)。 如果你尝试这么做,会遇到什么困难吗? |
其实我刚刚写完部分测试后,发现之前很多内容写的不够精简,又跑回去重构了,我刚开始写jest,你说的“不关注代码内部实现”,这个怎么理解,我写的时候,只想到了手动准备数据真的很繁琐,至于函数内部的细节肯定不会去思考了,对组件的test我还不知道怎么做,我现在做的是对一些支撑业务的函数进行test,比如对api进来的原始数据进行必要的排序呀、数据格式转换呀,因为我们原始数据基本不会给我们封装成业务格式的,他们要支持多端
…------------------ 原始邮件 ------------------
发件人: "Linesh"<notifications@github.com>;
发送时间: 2020年8月7日(星期五) 中午11:13
收件人: "linesh-simplicity/linesh-simplicity.github.io"<linesh-simplicity.github.io@noreply.github.com>;
抄送: "想"<1104295080@qq.com>; "Mention"<mention@noreply.github.com>;
主题: Re: [linesh-simplicity/linesh-simplicity.github.io] React 单元测试策略及落地 (#200)
我觉得既然有成效的测试关键是支持重构和自动化回归,那么只要能符合这个标准的测试就是好测试。你是先写测试,或者后补测试,都可以写出这样的测试来。
怎么做呢?我觉得做法上,1)尝试按测试三步曲来写(准备数据输入、调用方法、断言输出);2)让测试只关注“输入”和“输出”,而不关注代码内部实现(避免大量mock)。
如果你尝试这么做,会遇到什么困难吗?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
可以简单贴一段util或组件的产品代码(你可以隐去细节只留架子)吗?这样可以更好地讨论“不关注代码内部实现”。
“手动准备测试数据”很繁琐,这个有几种可能的原因,你可以看是否对的上:
|
OK,谢谢解答,晚点有时间可以贴些代码上来
…------------------ 原始邮件 ------------------
发件人: "Linesh"<notifications@github.com>;
发送时间: 2020年8月7日(星期五) 中午11:33
收件人: "linesh-simplicity/linesh-simplicity.github.io"<linesh-simplicity.github.io@noreply.github.com>;
抄送: "想"<1104295080@qq.com>; "Mention"<mention@noreply.github.com>;
主题: Re: [linesh-simplicity/linesh-simplicity.github.io] React 单元测试策略及落地 (#200)
可以简单贴一段util或组件的产品代码(你可以隐去细节只留架子)吗?这样可以更好地讨论“不关注代码内部实现”。
“api进来数据进行排序”这个听起来可以在reducer层做?可以参考上面reducer那节。
“数据格式转换”这个听起来可以放在组件的类方法里做/测试。咦,这部分我文章里没有写,可以贴点儿代码出来咱们看看
“手动准备测试数据”很繁琐,这个有几种可能的原因,你可以看是否对的上:
API数据/model字段很多。解决办法:
测试不关心的字段,你直接不准备就行了,还能提高测试表达力
测试不关心但实现又需要(比如会做存在校验/model validation等),可以抽取一些函数来给它们生成默认值
测试的是中间函数产生的数据。解决办法:能不能从更高层级的函数/端到端弄起来测,忽略中间过程,只看最终输入输出?
其他问题。
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
话说我最近上了个神仙HK项目,团队成员能力都很强(全栈、TDD、每个人都可以独立handle业务方案讨论、UI设计、技术方案实现、架构设计等全部软件开发流程)。有了这样的能力,团队可以把敏捷做成什么样子呢?就是基本不需要任何流程,所有时间都在产出方案和代码:
见过这样的团队后我感叹,果然人才是软件项目的基石啊。这套东西能work起来的基础,是对团队成员能力的信任,成员需要具备的基本能力:保证每个提交都能工作;保证每行生产代码都有有效的自动化测试覆盖。 “能力很强”这件事,我想跟内地的环境可能不一样:团队成员平均工作年龄10年+,最少的也有5年(就是在下我)…大公司有钱估计招的都是比较优秀的人,部分大公司软件成熟度也高一些,内地正处在发展阶段,整体成熟度没那么高,行业需求量大,组织和个人技能都不太可能聚集这么优秀的一些资源。 那么团队的自动化测试策略是怎么搭建的呢?跟我这篇文章说的有挺大不同,我也在重新反思这篇文章的一些假设和基础。这几天有空我随意更新下。 |
哈哈哈,我现在就在HK的项目里,他们交付要求都很严格,他们那边要求全栈多些,对自动化流程特别在意,测试是大于开发的
…------------------ 原始邮件 ------------------
发件人: "Linesh"<notifications@github.com>;
发送时间: 2020年8月7日(星期五) 中午12:00
收件人: "linesh-simplicity/linesh-simplicity.github.io"<linesh-simplicity.github.io@noreply.github.com>;
抄送: "想"<1104295080@qq.com>; "Mention"<mention@noreply.github.com>;
主题: Re: [linesh-simplicity/linesh-simplicity.github.io] React 单元测试策略及落地 (#200)
话说我最近上了个神仙HK项目,团队成员能力都很强(全栈、TDD、每个人都可以独立handle业务方案讨论、UI设计、技术方案实现、架构设计等全部软件开发流程)。有了这样的能力,团队可以把敏捷做成什么样子呢?就是基本不需要任何流程,所有时间都在产出方案和代码:
不需要IPM。一周一次showcase,showcase上PM会直接把优先级排好,大家按顺序领卡就是
不需要code review。CR按需进行,假设是团队成员都能交付高质量的代码
不需要retro。retro按需进行,三大会就砍了两个了,只维持了每日的站会
全员TDD。刚进项目时同事说,基本可以做到每行代码都有测试
没有QA。每个人都需要自动化测试自己的代码
每周上线。上线的时候只需要从当前master直接拉个跑过build的commit出来,就有信心release;release不需要手动回归,默认测试能跑过软件就没问题
小story不需要估点。因为你总是在交付最有价值的东西,并且以一周的粒度反馈/调整一次
见过这样的团队后我感叹,果然人才是软件项目的基石啊。这套东西能work起来的基础,是对团队成员能力的信任,成员需要具备的基本能力:保证每个提交都能工作;保证每行生产代码都有有效的自动化测试覆盖。
那么团队的自动化测试策略是怎么搭建的呢?跟我这篇文章说的有挺大不同,我也在重新反思这篇文章的一些假设和基础。这几天有空我随意更新下。
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
哈哈哈求更新自动化测试策略 |
可以转载吗 |
@old-zsh 可以的 |
@JimmyLv 你这个头像这么正式…要不我也来一个好了 |
2023/2024年最新版React组件单元测试最佳实践更新了!在这里:#230 5年功力和沉淀!快来捧场! |
React 单元测试策略及落地
续 #122 (comment)
写好的单元测试,对开发速度、项目维护有莫大的帮助。前端的测试工具一直推陈出新,而测试的核心、原则却少有变化。与产品代码一并交付可靠的测试代码,是每个专业开发者应该不断靠近的一个理想之地。本文就围绕测试讲讲,为什么我们要做测试,什么是好的测试和原则,以及如何在一个 React 项目中落地这些测试策略。
目录
children
型高阶组件为什么要做单元测试
虽然关于测试的文章有很多,关于 React 的文章也有很多,但关于 React 应用之详细单元测试的文章还比较少。而且更多的文章都更偏向于对工具本身进行讲解,只讲「我们可以这么测」,却没有回答「我们为什么要这么测」、「这么测究竟好不好」的问题。这几个问题上的空白,难免使人得出测试无用、测试成本高、测试使开发变慢的错误观点,导致在「质量内建」已渐入人心的今日,很多人仍然认为测试是二等公民,是成本,是锦上添花。这一点上,我的态度一贯鲜明:不仅要写测试,还要把单元测试写好;不仅要有测试前移质量内建的意识,还要有基于测试进行快速反馈快速开发的能力。没自动化测试的代码不叫完成,不能验收。
「为什么我们需要做单元测试」,这是一个关键的问题。每个人都有自己关于该不该做测试、该怎么做、做到什么程度的看法,试图面面俱到、左右逢源地评价这些看法是不可能的。我们需要一个视角,一个谈论单元测试的上下文。做单元测试当然有好处,但本文不会从有什么好处出发来谈,而是谈,在我们在意的这个上下文中,不做单元测试会有什么问题。
那么我们谈论单元测试的上下文是什么呢?不做单元测试我们会遇到什么问题呢?
单元测试的上下文
先说说问题。最大的一个问题是,不写单元测试,你就不敢重构,就只能看着代码腐化。代码质量谈不上,持续改进谈不上,个人成长更谈不上。始终是原始的劳作方式。
再说说上下文。我认为单元测试的上下文存在于「敏捷」中。现代企业数字化竞争日益激烈,业务端快速上线、快速验证、快速失败的思路对技术端的响应力提出了更高的要求:更快上线、更频繁上线、持续上线。怎么样衡量这个「更快」呢?那就是第一图提到的 lead time,它度量的是一个 idea 从提出并被验证,到最终上生产环境面对用户获取反馈的时间。显然,这个时间越短,软件就能越快获得反馈,对价值的验证就越快发生。这个结论对我们写不写单元测试有什么影响呢?答案是,不写单元测试,你就快不起来。为啥呢?因为每次发布,你都要投入人力来进行手工测试;因为没有测试,你倾向于不敢随意重构,这又导致代码逐渐腐化,复杂度使得你的开发速度降低。
再考虑到以下两个大事实:人员会流动,应用会变大。人员一定会流动,需求一定会增加,再也没有任何人能够了解任何一个应用场景。因此,意图依赖人、依赖手工的方式来应对响应力的挑战首先是低效的,从时间维度上来讲也是不现实的。那么,为了服务于「高响应力」这个目标,我们就需要一套自动化的测试套件,它能帮我们提供快速反馈、做质量的守卫者。唯解决了人工、质量的这一环,效率才能稳步提升,团队和企业的高响应力才可能达到。
那么在「响应力」这个上下文中来谈要不要单元测试,我们就可以很有根据了,而不是开发爽了就用,不爽就不用这样含糊的答案:
if-else
裸奔也不在话下,脑不好还做什么程序员,那你可以不用单元测试除此之外,你就需要写单元测试。如果你想随时整理重构代码,那么你需要写单元测试;如果你想有自动化的测试套件来帮你快速验证提交的完整性,那么你需要写单元测试;如果你是个长期项目有人员流动,那么你需要写单元测试;如果你不想花大量的时间在记住业务场景和手动测试应用上,那么你就需要单元测试。
至此,我们从「响应力」这个上下文中,回答了「为什么我们需要写单元测试」的问题。接下来可以谈下一个问题了:「为什么是单元测试」。
测试策略:测试金字塔
上面我直接从高响应力谈到单元测试,可能有的同学会问,高响应力这个事情我认可,也认可快速开发的同时,质量也很重要。但是,为了达到「保障质量」的目的,不一定得通过测试呀,也不一定得通过单元测试鸭。
这是个好的问题。为了达到保障质量这个目标,测试当然只是其中一个方式,稳定的自动化部署、集成流水线、良好的代码架构、组织架构的必要调整等,都是必须跟上的设施。我从未认为单元测试是解决质量问题的银弹,多方共同提升才可能起到效果。但相反,也很难想象单元测试都没有都写不好的项目,能有多高的响应力。
即便我们谈自动化测试,未必也不可能全部都是写单元测试。我们对自动化测试套件寄予的厚望是,它能帮我们安全重构已有代码、保存业务上下文、快速回归。测试种类多种多样,为什么我要重点谈单元测试呢?因为~~这篇文章主题就是谈单元测试啊…~~它写起来相对最容易、运行速度最快、反馈效果又最直接。下面这个图,想必大家都有所耳闻:
这就是有名的测试金字塔。对于一个自动化测试套件,应该包含种类不同、关注点不同的测试,比如关注单元的单元测试、关注集成和契约的集成测试和契约测试、关注业务验收点的端到端测试等。正常来说,我们会受到资源的限制,无法应用所有层级的测试,效果也未必最佳。因此,我们需要有策略性地根据收益-成本的原则,考虑项目的实际情况和痛点来定制测试策略:比如三方依赖多的项目可以多写些契约测试,业务场景多、复杂或经常回归的场景可以多写些端到端测试,等。但不论如何,整个测试金字塔体系中,你还是应该拥有更多低层次的单元测试,因为它们成本相对最低,运行速度最快(通常是毫秒级别),而对单元的保护价值相对更大。
以上是对「为什么我们需要的是单元测试」这个问题的回答。接下来一小节,就可以正式进入如何做的环节了:「如何写好单元测试」。
关于测试金字塔的补充阅读:测试金字塔实战。
如何写好单元测试:好测试的特征
写单元测试仅仅是第一步,下面还有个更关键的问题,就是怎样写出好的、容易维护的单元测试。好的测试有其特征,虽然它并不是什么新的东西,但总需要时时拿出来温故知新。很多时候,同学感觉测试难写、难维护、不稳定、价值不大等,可能都是因为单元测试写不好所导致的。那么我们就来看看,一个好的单元测试,应该遵循哪几点原则。
首先,我们先来看个简单的例子,一个最简单的 JavaScript 的单元测试长什么样:
以上就是一个最简答的单元测试部分。但麻雀虽小,五脏基本全,它揭示了单元测试的一个基本结构:准备输入数据、调用被测函数、断言输出结果。任何单元测试都可以遵循这样一个骨架,它是我们常说的 given-when-then 三段式。
为什么说单元测试说来简单,做到却不简单呢?除了遵循三段式,显然我们还需要遵循一些其他的原则。前面说到,我们对单元测试寄予了几点厚望,下面就来看看,它如何能达到我们期望的效果,以此来反推单元测试的特征:
下面来看看这三个原则都是咋回事:
有且仅有一个失败的理由
有且仅有一个失败的理由,这个理由是什么呢?是 「当输入不变时,当且仅当被测业务代码功能被改动了」时,测试才应该挂掉。为什么这会支持我们重构呢,因为重构的意思是,在不改动软件外部可观测行为的基础上,调整软件内部实现的一种手段。也就是说,当我被测的代码输入输出没变时,任我怎么倒腾重构代码的内部实现,测试都不应该挂掉。这样才能说是支持了重构。有的单元测试写得,内部实现(比如数据结构)一调整,测试就挂掉,尽管它的业务本身并没修改,这样怎么支持重构呢?不怪得要反过来骂测试成本高,没有用。一般会出现这种情况,可能是因为是先写完代码再补的测试,或者对代码的接口和抽象不明确所导致。
另外,还有一些测试(比如下文要看到的 saga 官方推荐的测试),它需要测试实现代码的执行次序。这也是一种「关注内部实现」的测试,这就使得除了业务目标外,还有「执行次序」这个因素可能使测试挂掉。这样的测试也是很脆弱的。
表达力极强
表达力极强,讲的是两方面:
这些表达力体现在许多方面,比如测试描述、数据准备的命名、与测试无关数据的清除、断言工具能提供的比对等。空口无凭,请大家在阅读后面测试落地时时常对照。
快、稳定
不快的单元测试还能叫单元测试吗?一般来讲,一个没有依赖、没有 API 调用的单元测试,都能在毫秒级内完成。那么为了达到快、稳定这个目标,我们需要:
在后面的介绍中,我会将这些原则落实到我们写的每个单元测试中去。大家可以时时翻到这个章节来对照,是不是遵循了我们说的这几点原则,不遵循是不是确实会带来问题。时时勤拂拭,莫使惹尘埃啊。
React 单元测试策略及落地
React 应用的单元测试策略
上个项目上的 React(-Native) 应用架构如上所述。它涉及一个常见 React 应用的几个层面:组件、数据管理、redux、副作用管理等,是一个常见的 React、Redux 应用架构,也是 dva 所推荐的 66%的最佳实践(redux+saga),对于不同的项目应该有一定的适应性。架构中的不同元素有不同的特点,因此即便是单元测试,我们也有针对性的测试策略:
对于这个策略,这里做一些其他补充:
关于不测 redux connect 过的组件这个策略。理由是成本远高于收益:要牺牲开发体验(搞起来没那么快了),要配置依赖(配置 store、
<Provider />
,在大型或遗留系统中补测试还很可能遇到@connect
组件里套@connect
组件的场景);然后收益也只是可能覆盖到了几个极少数出现的场景。得不偿失,果断不测。关于 UI 测试这块的策略。团队之前尝试过 snapshot 测试,对它寄予厚望,理由是成本低,看起来又像万能药。不过由于其难以提供精确快照比对,整个工作的基础又依赖于开发者尽心做好「确认比对」这个事情,很依赖人工耐心又打断日常的开发节奏,导致成本和收益不成正比。我个人目前是持保留态度的。
关于 DOM 测试这块的策略。也就是通过 enzyme 这类工具,通过 css selector 来进行 DOM 渲染方面的测试。这类测试由于天生需要通过 css selector 去关联 DOM 元素,除了被测业务外 css selector 本身就是挂测试的一个因素。一个 DOM 测试至少有两个原因可使它挂掉,并不符合我们上面提到的最佳实践。但这种测试有时又确实有用,后文讲组件测试时会专门提到,如何针对它制定适合的策略。
actions 测试
这一层太过简单,基本都可以不用测试,获益于架构的简单性。当然,如果有些经常出错的 action,再针对性地对这些 action creator 补充测试。
reducer 测试
reducer 大概有两种:一种比较简单,仅一一保存对应的数据切片;一种复杂一些,里面具有一些计算逻辑。对于第一种 reducer,写起来非常简单,简单到甚至可以不需要用测试去覆盖。其正确性基本由简单的架构和逻辑去保证的。下面是对一个简单 reducer 做测试的例子:
下面是一个较为复杂、更具备测试价值的 reducer 例子,它在保存数据的同时,还进行了合并、去重的操作:
reducer 作为纯函数,非常适合做单元测试,加之一般在 reducer 中做重逻辑处理,此处做单元测试保护的价值也很大。请留意,上面所说的单元测试,是不是符合我们描述的单元测试基本原则:
saveUserComments
时,应该与已有留言合并并去除重复的部分」;此外,测试数据只准备了足够体现「合并」这个操作的两条 id 的数据,而没有放很多的数据,形成杂音;selector 测试
selector 同样是重逻辑的地方,可以认为是 reducer 到组件的延伸。它也是一个纯函数,测起来与 reducer 一样方便、价值不菲,也是应该重点照顾的部分。况且,稍微大型一点的项目,应该说必然会用到 selector。原因我讲在这里。下面看一个 selector 的测试用例:
saga 测试
saga 是负责调用 API、处理副作用的一层。在实际的项目上副作用还有其他的中间层进行处理,比如 redux-thunk、redux-promise 等,本质是一样的,只不过 saga 在测试性上要好一些。这一层副作用怎么测试呢?首先为了保证单元测试的速度和稳定性,像 API 调用这种不确定性的依赖我们一定是要 mock 掉的。经过仔细总结,我认为这一层主要的测试内容有五点:
来自官方的错误姿势
redux-saga 官方提供了一个 util:
CloneableGenerator
用以帮我们写 saga 的测试。这是我们项目使用的第一种测法,大概会写出来的测试如下:这个方案写多了,大家开始感受到了痛点,明显违背我们前面提到的一些原则:
正确姿势
于是,针对以上痛点,我们理想中的 saga 测试应该:1) 不依赖实现次序;2) 允许仅对真正关心的、有价值的业务进行测试;3) 支持不改动业务行为的重构。如此一来,测试的保障效率和开发者体验都将大幅提升。
于是,我们发现官方提供了这么一个跑测试的工具,刚好可以用来完美满足我们的需求:
runSaga
。我们可以用它将 saga 全部执行一遍,搜集所有发布出去的 action,由开发者自由断言其感兴趣的 action!基于这个发现,我们推出了我们的第二版 saga 测试方案:runSaga
+ 自定义拓展 jest 的expect
断言。最终,使用这个工具写出来的 saga 测试,几近完美:这个测试略长,但它依然遵循 given-when-then 的结构。并且同样是测试「只保存获取回来的前三个推荐产品」、「对非 VIP 用户推送广告」两个关心的业务点,其中自有简洁的规律:
notImportant
的 action 是否被 dispatch 出去)expect(action).toHaveBeenDispatchedWith(payload)
matcher 很有表达力,且出错信息友好这个自定义的 matcher 是通过 jest 的
expect.extend
扩展实现的:上面是我们认为比较好的副作用测试工具、测试策略和测试方案。使用时,需要牢记你真正关心的业务价值点(本节开始提到的 5 点),以及做到在较为复杂的单元测试中始终坚守三大基本原则。唯如此,单元测试才能真正提升开发速度、支持重构、充当业务上下文的文档。
component 测试
组件测试其实是实践最多,测试实践看法和分歧也最多的地方。React 组件是一个高度自治的单元,从分类上来看,它大概有这么几类:
先把这个分类放在这里,待会回过头来谈。对于 React 组件测什么不测什么,我有一些思考,也有一些判断标准:除去功能型组件,其他类型的组件一般是以渲染出一个语法树为终点的,它描述了页面的 UI 内容、结构、样式和一些逻辑
component(props) => UI
。内容、结构和样式,比起测试,直接在页面上调试反馈效果更好。测也不是不行,但都难免有不稳定的成本在;逻辑这块,还是有一测的价值,但需要控制好依赖。综合「好的单元测试标准」作为原则进行考虑,我的建议是:两测两不测。组件的分支逻辑,往往也是有业务含义和业务价值的分支,添加单元测试既能保障重构,还可顺便做文档用;事件调用同样也有业务价值和文档作用,而事件调用的参数调用有时可起到保护重构的作用。
纯 UI 不在单元测试级别测试的原因,纯粹就是因为不好断言。所谓快照测试有意义的前提在于两个:必须是视觉级别的比对、必须开发者每次都认真检查。jest 有个 snapshot 测试的概念,但那个 UI 测试是代码级的比对,不是视觉级的比对,最终还是绕了一圈,去除了杂音还不如看 Git 的 commit diff。每次要求开发者自觉检查,既打乱工作流,也难以坚持。考虑到这些成本,我不推荐在单元测试的级别来做 UI 类型的测试。对于我们之前中等规模的项目,诉诸手工还是有一定的可控性。
连接 redux 的高阶组件不测。原因是,
connect
过的组件从测试的角度看无非几个测试点:mapStateToProps
中是否从store
中取得了正确的参数mapDispatchToProps
中是否地从actions
中取得了正确的参数props
是否正确地被传递给了组件props
触发组件进行一次更新这四个点,
react-redux
已经都帮你测过了,已经证明 work 了,为啥要重复测试自寻烦恼呢?当然,不测这个东西的话,还是有这么一种可能,就是你 export 的纯组件测试都是过的,但是代码实际运行出错。穷尽下来主要可能是这几种问题:mapStateToProps
中打错了字或打错了变量名mapStateToProps
但没有 connect 上去mapStateToProps
中取的路径是错的,在 redux 中已经被改过第一、二种可能,无视。测试不是万能药,不能预防人主动犯错,这种场景如果是小步提交发现起来是很快的,如果不小步提交那什么测试都帮不了你的;如果某段数据获取的逻辑多处重复,则可以考虑将该逻辑抽取到 selector 中并进行单独测试。
第三种可能,确实是问题,但发生频率目前看来较低。为啥呢,因为没有类型系统我们不会也不敢随意改 redux 的数据结构啊…(这侵入性重的框架哟)所以针对这些少量出现的场景,不必要采取错杀一千的方式进行完全覆盖。默认不测,出了问题或者经常可能出问题的部分,再策略性地补上测试进行固定即可。
综上,
@connect
组件不测,因为框架本身已做了大部分测试,剩下的场景出 bug 频率不高,而施加测试的话提高成本(准备依赖和数据),降低开发体验,模糊测试场景,性价比不大,所以强烈建议省了这份心。不测@connect
过的组件,其实也是 官方文档 推荐的做法。然后,基于上面第 1、2 个结论,映射回四类组件的结构当中去,我们可以得到下面的表格,然后发现…每种组件都要测渲染分支和事件调用,跟组件类型根本没必然的关联…不过,功能型组件有可能会涉及一些其他的模式,因此又大致分出一小节来谈。
@connect
业务型组件 - 分支渲染
对应的测试如下,测试的是不同的分支渲染逻辑:没有评论时,则不渲染 Comments header。
业务型组件 - 事件调用
测试事件的一个场景如下:当某条产品被点击时,应该将产品相关的信息发送给埋点系统进行埋点。
简单得很吧。这里的几个测试,在你改动了样式相关的东西时,不会挂掉;但是如果你改动了分支逻辑或函数调用的内容时,它就会挂掉了。而分支逻辑或函数调用,恰好是我觉得接近业务的地方,所以它们对保护代码逻辑、保护重构是有价值的。当然,它们多少还是依赖了组件内部的实现细节,比如说
find(TouchableWithoutFeedback)
,还是做了「组件内部使用了TouchableWithoutFeedback
组件」这样的假设,而这个假设很可能是会变的。也就是说,如果我换了一个组件来接受点击事件,尽管点击时的行为依然发生,但这个测试仍然会挂掉。这就违反了我们所说了「有且仅有一个使测试失败的理由」。这对于组件测试来说,是不够完美的地方。但这个问题无法避免。因为组件本质是渲染组件树,那么测试中要与组件树关联,必然要通过 组件名、id 这样的 selector,这些 selector 的关联本身就是使测试挂掉的「另一个理由」。但对组件的分支、事件进行测试又有一定的价值,无法避免。所以,我认为这个部分还是要用,只不过同时需要一些限制,以控制这些假设为维护测试带来的额外成本:
expect(component.find('div > div > p').html().toBe('Content')
的真的就算了吧如果你的每个组件都十分清晰直观、逻辑分明,那么像上面这样的组件测起来也就很轻松,一般就遵循
shallow
->find(Component)
-> 断言的三段式,哪怕是了解了一些组件的内部细节,通常也在可控的范围内,维护起来成本并不高。这是目前我觉得平衡了表达力、重构意义和测试成本的实践。功能型组件 -
children
型高阶组件功能型组件,指的是跟业务无关的另一类组件:它是功能型的,更像是底层支撑着业务组件运作的基础组件,比如路由组件、分页组件等。这些组件一般偏重逻辑多一点,关心 UI 少一些。其本质测法跟业务组件是一致的:不关心 UI 具体渲染,只测分支渲染和事件调用。但由于它偏功能型的特性,使得它在设计上常会出现一些业务型组件不常出现的设计模式,如高阶组件、以函数为子组件等。下面分别针对这几种进行分述。
utils 测试
每个项目都会有 utils。一般来说,我们期望 util 都是纯函数,即是不依赖外部状态、不改变参数值、不维护内部状态的函数。这样的函数测试效率也非常高。测试原则跟前面所说的也并没什么不同,不再赘述。不过值得一提的是,因为 util 函数多是数据驱动,一个输入对应一个输出,并且不需要准备任何依赖,这使得它非常适合采用参数化测试的方法。这种测试方法,可以提升数据准备效率,同时依然能保持详细的用例信息、错误提示等优点。jest 从 23 后就内置了对参数化测试的支持了,如下:
总结
好,到此为止,本文的主要内容也就讲完了。总结下来,本文主要覆盖到的内容如下:
@connect
过的高阶组件不测jest.extend
)、参数化测试等未尽话题 & 欢迎讨论
讲完 React 下的单元测试尚且已经这么花费篇幅,文章中难免还有些我十分想提又意犹未尽的地方。比如完整的测试策略、比如 TDD、比如重构、比如整洁代码设计模式等。如果读者有由此文章而生发、而疑虑、而不吐不快的种种兴趣和分享,都十分欢迎留下你的想法和指点。写文交流,乐趣如此。感谢。
附录:快照测试成本与实践落地剖析
snapshot testing 测试实践
之前我对快照测试是很有看法的,觉得外界对它「多快好省」的希冀其实真实的价值并没有那么大,反而有很多副作用。就像 TDD 的狂热者一样,大家对快照测试狂热不已。为了描述这种坏味道,我有三点主要的质疑:
这些质疑都有其道理,却也不是不能解决。先来看看其道理所在:反 TDD 很明显,它是写完再打快照,那在你开始写到写完这个过程,你没有办法通过测试获得反馈;阻碍重构,是指我重构组件的过程中就算逻辑没有动,但因为字符串的改变组件仍然会挂(这点非真);损伤开发者体验,主要是说如果我频繁运行测试,并且正在修改组件,那我不得不重复「比对差异 - 更新快照」这个事情以使快照测试通过,否则我就得忍受经常红掉的测试。
先说反 TDD 这个事情,它确实和 TDD 不是一路的玩法。和祁兮沟通了一下,他觉得快照测试更接近 ATDD 这样高层级的测试,只不过它是针对于 UI 的测试。那可能就是你先写着,但在你的故事卡完成之前它一直会是挂的。那么你是不是可以选择在做卡的时候不去运行快照测试,只有结卡的时候再去把它固化下来?这点看起来很吸引,但仍然有一个问题,且看后面。但关键点是说,它确实不是与 TDD 兼容的测试方式,但同时它也是更高层级的测试,你不需要经常运行,需要 TDD 的时候依然可以采用其他的测试工具。
阻碍团队重构,是我了解不足所导致。我原本以为,只有改了任意字符串,快照测试就会挂,哪怕你没有改动功能,那这样就不满足我们对「不改变软件可观测行为的前提下进行重构」的标准。但事实证明,快照测试并不只是简单比较字符串,而且比较对于给定的 props 输入,输出是不是一致。因此,里面的可执行代码(逻辑)变化了,只要最终可观测的输出没有变化,那么快照测试也不会挂球。所以这点感受非真。
损伤开发者体验,这个问题比较关键。因为快照测试是检测「变化」,而非「结果」,所以当你改动了 UI 时,快照测试必然会挂。这个测试的挂就是给你传达这样一个信息:「现在我挂了,说明你改了代码。至于你改对了没有,那我咋个晓得呢,你确定你真的要修改吗?」因此,你要做的无非两个事情:比对差异、做出回应(更新快照或回滚修改)。这个对开发者体验可能的影响点是在,它是凭空地在你的红绿循环中间加了这么一道,当你频繁运行测试时,你可能必须频繁重复这两个事情:比对差异、做出回应。那么,你要么别频繁运行测试,要么能快速地完成这两个事情。
别频繁运行测试肯定是不能接受的,那么问题就转化成了:如何快速地完成这两个事情,以及这个过程带来的价值真的比开发者体验过程牺牲的反馈时间性价比更高吗?
价值。我们把组件的观测点分为两类:纯 UI 和逻辑。逻辑方面,用 Enzyme 来覆盖更加能保留业务场景;UI 方面,用 Enzyme 来覆盖必然要面临脆弱测试的问题,此时用 snapshot 测试则更有价值。因此我们讨论的结果是 snapshot 测试主要的价值就是在固化 UI 上,这其中也包括样式。祁兮说,如果把 UI 当成与业务代码一样对待,那么保证每个提交都没有破坏业务功能和 UI,显示是有其价值的。
如何快速地完成 比对差异 和 做出回应 这两个事情:
比对差异
对于 UI 组件的更新,一般可以分为四类操作:
后两个在纯 React/HTML/JSX 中体验过,也跟纯文本对比一样,所以基本来说可以达到没什么成本。尽管说,这样而言就跟 Git 比对没什么两样,为啥还要测试这个流程?优点还是有一个,可以自动化,以及防止人为对 UI 的疏忽导致改出来 bug。问题是,在 RN 中,增删组件、包一层减一层组件(一般是包
View
),jest 会默认把 View 的内部实现展开出来,变成冗长的字符串,这会导致差异没法比对,大大增加比对成本。解决方案是把它 mock 掉。因此,差异比对这一块,如果没有特别的坑,可以做到比较低的成本;比对方式,仍然遵照 Git 进行文本比对的方式即可。
更新快照
jest --updateSnapshot
,但执行太耗时,并且要它执行完以后你才可以 commit,需要等待,打断节奏;jest --watch
的方式,在快照更新并确认要更新后按个w
去更新,但我用的是 WebStorm 的测试面板,它通常被我放在副屏,不在主屏,这样我要么需要通过cmd + 4
或鼠标移动的方式切换到测试面板去执行这个「按w
更新快照」的操作,然后再通过两下escape
或鼠标移动点击的方式回主面板。当我核心注意力在红绿循环中的时候,这无疑需要额外的注意力来切换屏幕、进行鼠标操作,乃是大忌,频繁执行测试下,对工作流和节奏的打乱更要命precommit
脚本自动执行jest --updateSnapshot
。这是我目前想到相对最好的解决方案,在你比对了差异并确认需要更新后,还是通过 WS 的 Git 插件cmd + k
进行 commit,然后 hook 脚本就会在后台执行,既不需要切换,也不需要打断当前思路和流程至此,如果没有其他的坑,差异比对和更新快照的问题都得到了妥善解决。去除了这层成本对开发者体验的影响,我依然可以 TDD,于是能尝试去使用快照测试,看看具体的关节实践起来是否真如我们期望。
还有另外一个问题,就是快照测试什么时候运行并提交的问题。有三种粒度:
jest --updateSnapshot
了,可能会增加比对差异的成本;综上,快照测试的价值是对 UI 进行覆盖,把 UI 当成与逻辑同等地位的代码来看待。同时,快照测试不能完全取代单元测试,有逻辑的部分建议使用 Enzyme 进行 TDD 和覆盖。快照测试的粒度,建议是 commit 的粒度,以便每次提交都能保持「原子性」(即 lint、UT、快照测试都通过)。为此,依然需要频繁运行测试,并需要解决这个过程频繁的「差异比对」和「更新快照」可能带来的过高的成本,防止开发者体验因此降低。在价值、定位、粒度、具体操作都明确的基础上,不妨进行尝试,看它是否能带来预期的收益。
https://benmccormick.org/2016/09/19/testing-with-jest-snapshots-first-impressions 。这里将 ST 的优缺点和利弊权衡都讲得好。
施工中 🚧 TODOLIST
Readable
例子补充一下。这种模式,是不是都可以写个专门的 helper 来屏蔽掉这些细节?是不是可以把里面的 children 直接抽取出一个组件来测试逻辑?TagsContainer/index.test.js
有个也还不错的测试ImproveUserInfo
上有许多不错的测试用例,可惜设计和耦合比较深,难以剥离出来Login
里面也有些「验证登录按钮可不可点」的逻辑,非常适合做单元测试,但写的还是有点依赖于实现ProductDetail
里面好像也有很多逻辑,然而没写好Register
里关于测试六个字段的部分,也有点意思,它可能违反了最佳实践,但它是从 tasking 列表来的;Register
关于GetValidationCode
的部分,也有点问题,是个通用问题:大多数情况下,你不应该直接去操作内部字段,而是应该通过「界面行为」去表达场景,让内部自己处理,然后你最后得到断言结果Reservations
里也有有趣的实践DevFeatures
里提供了一个实例,如何在一个测试中需要多次用到一个 mock,并且返回不同的值Visible
这样的组件,可读性提升了,consumer 端怎么测试?如果只测传给Visible
的参数,显然就变成测试实现了moment()
Date.now
等整理中:Readable 例子
功能型组件 - 以函数为子组件
既然是侧重逻辑的功能型组件,它的设计模式就比较多样一些,其中经常会出现「以函数为子组件」的这种设计模式。至于为什么会用到这种模式,它的利弊如何呢,程墨有本书《深入浅出 React 和 Redux》,讲的很到位,这里不再细补充。还是以代码为例子:
这个组件,顾名思义,是负责管理「已读未读」的组件:它接受一个 children,负责记录它是否已被点击(阅读)过的状态,并将此状态作为参数,调用 children 时传递给它。再来看使用它的地方:
好,现在比如你要测
ProductItem
里面的这段「是否渲染评论组件」的逻辑,你要怎么测呢?一般的shallow(<ProductItem />)
,里面这段以函数作为子组件的函数可不会被调用哟?目前我们项目解决方案是,手动拿到 children 这个函数,再手动
shallow
渲染一下,然后再测。再次地,这非常有侵入性,对实现了解比上面的例子更多。这是我所能接受的差不多一个平衡点了,就是如果准备再复杂一些,我就会开始觉得麻烦了。The text was updated successfully, but these errors were encountered: