ethanlin-twer.github.io icon indicating copy to clipboard operation
ethanlin-twer.github.io copied to clipboard

React单元测试最佳实践与前端TDD

Open EthanLin-TWer opened this issue 1 year ago • 10 comments

本Github issue主要是为了方便留言。最新文章内容请移步我的博客(需要能够同时访问Github和jsDelivr,不便之处抱歉了…)

https://ethan.thoughtworkers.me/#/post/2023-12-10-react-unit-testing-best-practices-v2

EthanLin-TWer avatar Jan 14 '24 02:01 EthanLin-TWer

地址 404 了诶

WanQuanXie avatar Jan 14 '24 14:01 WanQuanXie

修好啦。其实是build的时候不知道为啥把所有静态资源都删了😂重新build了一下就可以了。

😘

WanQuanXie avatar Jan 15 '24 05:01 WanQuanXie

最新内容上线了朋友们 @JimmyLv @WanQuanXie 快来围观!哈哈哈,都4202年了,还有人在写React吗

EthanLin-TWer avatar Jan 17 '24 14:01 EthanLin-TWer

@EthanLin-TWer 谢谢分享,干货很多!本文提倡从最上层的 React 组件入手,除了 mock 三方依赖之外,通过贯穿整个应用的单元测试策略来进行测试。有时候某些 ui-components, Domain Logic 足够复杂,我想本文测试策略是不是还不够覆盖到每个细节🤔(所以在你的示例代码中有对 ui-components 层的额外单元测试 ),我有个疑问,我们应该如何平衡「贯穿整个应用」和「额外为某一层增加测试」之间的关系,让我们整个系统的单元测试是充分的而又不是重叠的?我们应该对哪些层额外添加测试,哪些层放在 routes 层一并测试?

另外,在对 ui-components 层的单元测试中,有调用放在 routes 层级下的 tester import { findCounter } from '../../../routes/__tests__/component-testers/counter.tester',是不是有一种打破分层的感觉 👀

dukeluo avatar Jan 18 '24 09:01 dukeluo

另外,在对 ui-components 层的单元测试中,有调用放在 routes 层级下的 tester import { findCounter } from '../../../routes/__tests__/component-testers/counter.tester',是不是有一种打破分层的感觉 👀

@dukeluo 先回复个简单的。点个赞,代码看得真细!是的,这里我有点偷懒了,其实我在想整个routes/__tests__/下的api-mocks/business-testers/component-testers/fixtures/都挪到一个公共的地方去(比如test/根目录下)会更好一些,这样就可以解除这个不恰当的依赖关系。

只能在代码demo里改了,博客代码和截图重新弄effort太高了哈哈哈🚬

EthanLin-TWer avatar Jan 18 '24 10:01 EthanLin-TWer

我们应该如何平衡「贯穿整个应用」和「额外为某一层增加测试」之间的关系,让我们整个系统的单元测试是充分的而又不是重叠的?我们应该对哪些层额外添加测试,哪些层放在 routes 层一并测试?

@dukeluo 把“额外为某一层增加测试”这个问题拆解来看,我想最主要的是关于React Hooks的:什么时候使用这种page tests直接测,什么时候把一些逻辑放到React Hooks里头做单测?这也是我下一篇React Hooks最佳实践与面向对象目前还卡住的地方。

为什么单提React Hooks呢?因为其他的层级似乎已经有相对成熟的答案了:

  • utils:比如时间、格式转换这种的。因为这种单测没难度,我相信一直以来项目上的小伙伴也都会针对这些utils做更细致的测试覆盖。
  • ui-components:这个我设想应该放的是每个项目自己(要么基于现有库封装、要么自己从新写)搞的一套通用UI组件。那么这些组件自然也需要对它的逻辑功能做更全面的覆盖,该补就补。比如,我的业务只提到可以选时间,但是作为一个完整的Date(Range)Picker组件会有更多或一致性或功能性的要求,比如不能允许end date选得比start date还早啦,之类不一定在AC里体现的要求,这些就需要额外增加测试去覆盖。这层也没疑问。
  • redux/数据管理:这部分我的实践经验就是,如果reducer里有重逻辑或者分支那自然推荐测一测。如果没有,也不建议测试,因为单测action有没有发对、单个reducer有没有搞对其实没啥意思,有点在帮忙测redux的感觉;其次是这些测试是特定于redux这个“技术实现”的,一旦换工具(在以往的项目上真实发生)这些测试都得rework。性价比不高。通过page tests拉到一起测起来就可以了。
  • 还有什么组件?欢迎探讨。

所以说我现在直觉本质问题还是有些业务逻辑是写单独的React Hooks单测还是一样通过page tests覆盖,是吧?这其实是个性价比问题。我现在还没有成熟的想法或指导原则,再想想。不过目前做法就是page tests能测的话就它测问题不大,React hooks想增加单测也可以,只要够稳定,否则还要平衡改动成本。测试之间有一些重叠也可以接受,因为page tests更像发现问题的测试,而React hooks是定位问题的测试,目的不同,可以共存。

EthanLin-TWer avatar Jan 18 '24 10:01 EthanLin-TWer

所以说我现在直觉本质问题还是有些业务逻辑是写单独的React Hooks单测还是一样通过page tests覆盖,是吧?

@EthanLin-TWer 是的, React Hooks 以及一些 Domain Object,期待你的下一篇 :)

dukeluo avatar Jan 18 '24 13:01 dukeluo

非常好的文章,感谢分享 👍 文中观点我都非常认同,而且你通过示例代码来说明的过程也很清晰,学习到了。 我自己也写前端,没你那么专业,只是想做到全栈开发而已。我用的是VueJS 2,因为项目中会写UI端到端测试的缘故,所以前端的ut最近写的不多,只有遇到复杂的ui交互逻辑才会写。当然写的时候,和你推荐的方法一样,从页面组件入手,只会mock掉api,同样也能做到tdd的效果。

提一个小的建议哈。那个 hotels list 下面 should call search endpoint with correct parameters: city id, check dates in yyyy-MM-dd, and no. of occupancies 的测试,我觉得有个地方可以改进一下。就是把BeforeEach中的 api mock 数据写到测试中去,并且加上具体的属性值,以便和 expect中验证的数据形成对应关系。大致代码修改如下:

it('should render available hotels once loaded with correct information:' +
    'hotel name, address, stars, user rating, number of user ratings and lowest price', () => {
    hotelListPageDSL.mockGetHotelListOnce([
        createHotel(hotelMocks[0], { name: '杭州栖湖轻奢酒店', ... }),
        createHotel(hotelMocks[0], { name: '杭州中山西子湖酒店', ... })
      ])

    renderHotelList(
      <HotelList />,
      '/hotels/list?city=HZ&checkinDate=2024-01-20&checkoutDate=2024-01-28&noOfOccupancies=2'
    )

    expect(getHotelList()).toEqual([
      ['杭州栖湖轻奢酒店', '西湖湖滨商圈', '★★★★', '用户评分:4.2', '930条点评', '¥198起'],
      ['杭州中山西子湖酒店', '西湖湖滨商圈', '★★★★★', '用户评分:4.7', '317条点评', '¥498起'],
    ])
  })

正如你文中提到的,测试要表达力强。我认为测试中体现准备数据(这里就是 api 返回的hotel数据)和验证数据之间的关系是非常重要的。原来的测试给人一种感觉,就是我知道要验证什么结果,但是我看不出来”为什么“是这样的结果。 其实,你在下个测试里面就给出了准备数据的环节 hotelListPageDSL.mockGetHotelListOnce(createHotel(hotelMocks[9], { name: '杭州华辰国际饭店', noOfUserRatings: 96 })])。我想或许你在上个测试中把准备数据放到BeforeEach有你的考量,愿闻其详 😄

最后,我尤其认同文中提到构筑测试代码 DSL的做法。我的伙伴 @leeonky 给端到端测试开发了一些用于准备数据和验证数据的DSL 的开源库。如果你有兴趣看看给点反馈的话,非常欢迎哈。链接这里就不贴了,不然太喧宾夺主,可以私聊 😄

JosephYao avatar Jan 19 '24 09:01 JosephYao

我想或许你在上个测试中把准备数据放到BeforeEach有你的考量,愿闻其详 😄

正如你所说,测试中体现准备数据和验证数据之间的关系是非常重要的。在第一个测试中我没有把完整的测试数据摆出来,不是非常刻意的选择,不过潜意识里我是觉得全部数据放上来,测试数据的准备部分就变长了,可能会触发linter工具的自动换行,从而让测试的given部分变得冗长、失去重点。所以trade off的方法是,我给这个测试数据起了个名exampleTwoHotels,以表示这是基准的测试数据,所有测试都会用它,所以读者可以不必太关注这里面的值。以此在“为什么是这个断言数据”的表达性和“given数据”的表达性中做个取舍。

最后,我尤其认同文中提到构筑测试代码 DSL的做法。我的伙伴 @leeonky 给端到端测试开发了一些用于准备数据和验证数据的DSL 的开源库。如果你有兴趣看看给点反馈的话,非常欢迎哈。链接这里就不贴了,不然太喧宾夺主,可以私聊 😄

👍这里就是个大家交流的地方,不用客气可以直接贴链接嘛,哈哈。

最后感谢阅读以及花时间给我写一些反馈! @JosephYao

EthanLin-TWer avatar Jan 29 '24 07:01 EthanLin-TWer

多谢你的回复哈。我们有两个核心的开源库,一个涉及到数据准备 https://github.com/leeonky/jfactory , 另一个则是用来做验证的 https://github.com/leeonky/DAL-java。目的就是让given和then的能够通过dsl写的更加简单,突出重点,忽略测试中那些不必要的细节。而这些对单元测试和端到端测试都是适用的。

不过,这两个库都是java写的,暂时没有其他语言的版本,对于前端js开发来说,只能算是个参考了 😄 。围绕这两个核心库还有一些周边的辅助库,比较多,先就不列举了。

期待你的反馈哈,也同样期待你后续的文章 👍

JosephYao avatar Jan 29 '24 08:01 JosephYao