51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 2761|回复: 1
打印 上一主题 下一主题

[转贴] 网易云团队前端单元测试技术方案总结(四)

[复制链接]
  • TA的每日心情
    无聊
    9 小时前
  • 签到天数: 937 天

    连续签到: 4 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2021-8-4 10:14:47 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    Shallow 渲染
      因为 shallow 模式仅能感知到第一层自定义子组件组件,往往只能用于简单组件[url=]测试[/url]。例如下面的组件:
    1.  // js/avatar.js
    2.   function Image({ src }) {
    3.       return <img src={src} />;
    4.   }
    5.   function Living({ children }) {
    6.       return <div className="icon-living"> { children } </div>;
    7.   }
    8.   function Avatar({ user, onClick }) {
    9.       const { living, avatarUrl } = user;
    10.       return (
    11.           <div className="container" onClick={onClick}>
    12.               <div className="wrapper">
    13.                 <Living >
    14.                   <div className="text"> 直播中 </div>
    15.                 </Living>
    16.               </div>
    17.               <Image src={avatarUrl} />
    18.           </div>
    19.       )
    20.   }
    21.   export default Avatar;
    复制代码
    shallow 渲染虽然不是真正的渲染,但是其组件生命周期会完整地走一遍。
      使用 shallow(<Avatar />)  能感知到的结构如下, 注意看到 div.text 作为 Living 组件的 children 能够被检测到,但是 Living 的内部结构无法感知。

     Enzyme 支持的选择器支持我们熟悉的 css selector 语法,这种情况下我们可以对 DOM 结构做如下测试:
    1.  // test/avatar.test.js
    2.   import Avatar from '../js/avatar';
    3.   describe('Avatar', () => {
    4.       let wrapper = null, avatarUrl = 'abc';
    5.       beforeEach(() => {
    6.           wrapper = shallow(<Avatar user={{ avatarUrl: avatarUrl }} />);
    7.       })
    8.       afterEach(() => {
    9.           wrapper.unmount();
    10.           jest.clearAllMocks();
    11.       })
    12.       it ('should render success', () => {
    13.           // wrapper 渲染不为空
    14.           expect(wrapper).not.toBeEmptyRender();
    15.           // Image 组件渲染不为空, 这里会执行 Image 组件的渲染函数
    16.           expect(wrapper.find('Image')).not.toBeEmptyRender();
    17.           // 包含一个节点
    18.           expect(wrapper).toContainMatchingElement('div.container');
    19.           // 包含一个自定义组件
    20.           expect(wrapper).toContainMatchingElement("Image");
    21.           expect(wrapper).toContainMatchingElement('Living');
    22.           // shallow 渲染不包含子组件的内部结构
    23.           expect(wrapper).not.toContainMatchingElement('img');
    24.           // shallow 渲染包含 children 节点
    25.           expect(wrapper).toContainMatchingElement('div.text');
    26.           // shallow 渲染可以对 children 节点内部结构做测试
    27.           expect(wrapper.find('div.text')).toIncludeText('直播中');
    28.       })
    29.   })
    复制代码
    如果我们想去测试对应组件的 props / state 也可以很方便测试,不过目前存在缺陷,Class Component 能通过 toHaveProp, toHaveState 直接测试, 但是 Hook  组件无法测试 useState 。
    1.  it ('Image component receive props', () => {
    2.     const imageWrapper = wrapper.find('Image');、
    3.     // 对于 Hook 组件目前我们只能测试 props
    4.     expect(imageWrapper).toHaveProp('src', avatarUrl);
    5.   })
    复制代码
    wrapper.find 虽然会返回同样的一个 ShallowWrapper 对象,但是这个对象的子结构是未展开的,如果想测试imageWrapper 内部结构,需要再 shallow render 一次。
    1.  it ('Image momponent receive props', () => {
    2.     const imageWrapper = wrapper.find('Image').shallow();
    3.     expect(imageWrapper).toHaveProp('src', avatarUrl);
    4.     expect(imageWrapper).toContainMatchingElement('img');
    5.     expect(imageWrapper.find('img')).toHaveProp('src', avatarUrl);
    6.   })
    复制代码
    也可以改变组件的 props, 触发组件重绘。
    1. it ('should rerender when user change', () => {
    2.       const newAvatarUrl = '' + Math.random();
    3.       wrapper.setProps({ user: { avatarUrl: newAvatarUrl }});
    4.       wrapper.update();
    5.       expect(wrapper.find('Image')).toHaveProp('src', newAvatarUrl);
    6.   })
    复制代码
    另一个常见的场景是事件模拟,事件比较接近真实测试场景,这种场景下使用 shallow 存在诸多缺陷,因为 shallow 场景事件不会像真实事件一样有捕获和冒泡流程,所以此时只能简单的触发对应的 callback 达到测试目的。
    1.  it ('will call onClick prop when click event fired', () => {
    2.       const fn = jest.fn();
    3.       wrapper.setProps({ onClick: fn });
    4.       wrapper.update();
    5.       // 这里触发了两次点击事件,但是 onClick 只会被调用一次。
    6.       wrapper.find('div.container').simulate('click');
    7.       wrapper.find('div.wrapper').simulate('click');
    8.       expect(fn).toHaveBeenCalledTimes(1);
    9.   })
    复制代码
    关于这些网上有人总结了 shallow 模式下的一些不足:
      1.hallow 渲染不会进行事件冒泡,而 mount 会。
      2.shallow 渲染因为不会创建真实 DOM,所以组件中使用 refs 的地方都无法正常获取,如果确实需要使用 refs , 则必须使用 mount。
      3.simulate  在 mount 中会更加有用,因为它会进行事件冒泡。
      其实上面几点说明了一个现象是 shallow 往往只适合一种理想的场景,一些依赖[url=]浏览器[/url]行为表现的操作 shallow 无法满足,这些和真实环境相关的就只能使用mount了。
      Mount 渲染
      Mount 渲染的对象结构为 ReactWrapper 其提供了和 ShallowWrapper 几乎一样的 API , 差异很小。
      在 API层面的一些差异如下:
    1. + getDOMNode()        获取DOM节点
    2.   + detach()            卸载React组件,相当于 unmountComponentAtNode
    3.   + mount()             挂载组件,unmount之后通过这个方法重新挂载
    4.   + ref(refName)        获取 class component 的 instance.refs 上的属性
    5.   + setProps(nextProps, callback)
    6.   - setProps(nextProps)
    7.   - shallow()
    8.   - dive()
    9.   - getElement()
    10.   - getElements()
    复制代码
     另外由于 mount 使用 ReactDOM 进行渲染,所以其更加接近真实场景,在这种模式下我们能观察到整个 DOM 结构和React组件节点结构。

    1.  describe('Mount Avatar', () => {
    2.       let wrapper = null, avatarUrl = '123';
    3.       beforeEach(() => {
    4.           wrapper = mount(<Avatar user={{ avatarUrl }} />);
    5.       })
    6.       afterEach(() => {
    7.           jest.clearAllMocks();
    8.       })
    9.       it ('should set img src with avatarurl', () => {
    10.           expect(wrapper.find('Image')).toExist();
    11.           expect(wrapper.find('Image')).toHaveProp('src', avatarUrl);
    12.           expect(wrapper.find('img')).toHaveProp('src', avatarUrl);
    13.       })
    14.   })
    复制代码
    在 shallow 中无法模拟的事件触发问题在 mount 下就不再是问题。
    1. it ('will call onClick prop when click event fired', () => {
    2.       const fn = jest.fn();
    3.       wrapper.setProps({ onClick: fn });
    4.       wrapper.update();
    5.       wrapper.find('div.container').simulate('click');
    6.       wrapper.find('div.wrapper').simulate('click');
    7.       expect(fn).toHaveBeenCalledTimes(2);
    8.   })
    复制代码
    总结一下 shallow 中能做的 mount 都能做,mount中能做的 shallow 不一定能做。
      Render 渲染
      render 内部使用 react-dom-server 渲染成字符串,再经过 Cherrio 转换成内存中的结构,返回 CheerioWrapper 实例,能够完整地渲染整个DOM 树,但是会将内部实例的状态丢失,所以也称为 Static Rendering 。这种渲染能够进行的操作比较少,这里也不作具体介绍,可以参考 官方文档
      总结
      如果让我推荐的话,对于真实浏览器我会推荐 Karma + Jasmine 方案测试,对于 React 测试 Jest + Enzyme 在 JSDOM 环境下已经能覆盖大部分场景。另外测试 React组件除了 Enzyme 提供的操作, Jest 中还有很多其他有用的特性,比如可以 mock 一个 npm 组件的实现,调整 setTimeout 时钟等,真正进行[url=]单元测试[/url]时,这些工具也是必不可少的,整个单元测试[url=]技术[/url]体系包含了很多东西,本文无法面面俱到,只介绍了一些距离我们最近的相关的技术体系。




    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-4-25 18:28 , Processed in 0.069072 second(s), 24 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表