用React hooks实现TDD

时间: 2020-02-18阅读: 839标签: hooks

由于篇幅所限文章中并没有给出demo的所有代码,大家如果有兴趣可以将代码clone到本地从commit来看整个demo的TDD过程,配合文章来看会比较清晰。本文涉及的所有代码地址: teobler/TDD-with-React-hooks-demo


前端TDD的痛

从进公司前认识了TDD,到实践TDD,过程中自己遇到或者小伙伴们一起讨论的比较频繁的一个问题是 — 前端不太好TDD / 前端TDD的投入收益比不高。为啥会这样呢?

我们假设你在写前端时全程TDD,那么你需要做的是 — 先assert页面上有一个button,然后去实现这个button,之后assert点击这个button之后会发生什么,最后再去实现相应的逻辑。

这个过程中有一个问题,因为前端中UI和逻辑强耦合,所以在TDD的时候你需要先实现UI,然后选中这个UI上的组件,trigger相应的行为,这个过程给开发人员增加了不少负担。

诚然,这样写出来的代码严格遵循了TDD的做法,也得到了TDD给我们带来的各种好处,但是据我观察下来,身边的小伙伴们没有一个人认同这样的做法。大家的痛点在于UI部分的TDD过于痛苦并且收益太低,而且由于UI和逻辑强耦合,后续的逻辑部分也需要先选取页面上的元素trigger出相应的执行逻辑。

这些痛点在项目组引入了hooks之后有了显著的改善,从引入hooks到现在快一年的时间,组里的小伙伴们一起总结除了一套测试策略。在此我们将react的组件分为三类 — 纯逻辑组件(比如request的处理组件,utils函数等),纯UI组件(比如展示用的Layout,Container组件等)和两者结合的混合组件(比如某个页面)。


纯逻辑组件

这部分组件没啥好说的,全都是逻辑,tasking,测试,实现,重构一条龙,具体咋写我们这里不讨论。

// combineClass.test.ts
describe('combineClass', () => {
    it('should return prefixed string given only one class name', () => {
        const result = combineClass('class-one');
        expect(result).toEqual('prefix-class-one');
    });

    it('should trim space for class name', () => {
        const result = combineClass('class-one ');
        expect(result).toEqual('prefix-class-one');
    });

    it('should combine two class name and second class name should not add prefix', () => {
        const result = combineClass('class-one', 'class-two');
        expect(result).toEqual('prefix-class-one class-two');
    });

    it('should combine three class name and tail class name should not add prefix', () => {
        const result = combineClass('class-one', 'class-two', 'class-three');
        expect(result).toEqual('prefix-class-one class-two class-three');
    });
});

// combineClass.ts
const CLASS_PREFIX = "prefix-";
export const combineClass = (...className: string[]) => {
    const resultName = className.slice(0);
    resultName[0] = CLASS_PREFIX + className[0];

    return resultName
        .join(' ')
        .trim();
};


纯UI组件

这类组件我们没有一个个去测试组件里面的元素,而是按照UX的要求build完组件以后加上一个jest的json snapshot测试。

注意这里的snapshot并不是大家印象中的e2e测试中的截图,而是jest里将组件render出来之后使用json生成一份UI的dom结构,在下次测试时,生成一份新的快照与旧的快照进行比对,从而得出两个UI不一样的地方,实现对UI的保护。

但是其实使用snapshot测试有两个问题:

  1. snapshot相比较于一般的单元测试来说运行速度较慢,如果项目中大量使用的snapshot测试的话,在运行所有单元测试的时候会比较明显的感受到单元测试的速度被拖慢了,一定程度上违背了单元测试快速反馈的初衷;
  2. 维护snapshot的人工成本较大,snapshot测试最大的问题在于你只要改动了任何UI的部分,这个测试都会挂掉,这个时候就需要仔细对比不同的地方以决定是更新snapshot还是改错地方了,而如果此时团队里有“省心”的队友无脑更新snapshot的话,这个测试相当于浪费了资源。
    // Content.test.tsx
    describe('Content', () => {
        it('should render correctly', () => {
            const {container} = render(<Content/>);
            expect(container).toMatchSnapshot();
        });
    });
    
    // Content.test.tsx.snap
    // Jest Snapshot v1, https://goo.gl/fbAQLP
    
    exports[`Content should render correctly 1`] = `
    <div>
      <main
       
      />
    </div>
    `;
    
    // Content.tsx
    export const Content: react.FC<React.htmlAttributes<htmlElement>> = (props) => {
        const { className = '', children, ...restProps } = props;
    
        return (
            <main className={combineClass('layout-content', className)} {...restProps}>
                {children}
            </main>
        );
    };


逻辑与UI混合组件

这个部分我们就需要hooks的帮忙了,这样的组件不是UI和逻辑强耦合嘛,那我们就可以将两者拆开。于是这样的组件我们会这样写:

  1. 首先将UI页面build出来,但是需要的callback全部写成空函数
  2. 将所有callback或者是页面需要用到的逻辑抽到一个hook中
  3. 此时hook里的代码没有UI只有逻辑,故可以使用测试库对hook进行单独的逻辑测试,所以此时hook的开发可以按照逻辑组件的开发进行TDD
  4. 整个混合组件开发完成后,补上一个snapshot测试,需要注意的是可能该组件在渲染时需要一些数据,在写snapshot测试时应该确保准备的数据是完备的,否则快照会渲染出一份根本没有数据的错误组件
    // usePageExample.test.ts
    import {act, renderHook} from "@testing-library/react-hooks";
    
    describe('usePageExample', () => {
        let mockGetUserId: jest.Mock;
    let mockValidate: jest.Mock;
    
    
    beforeAll(() => {
        mockGetUserId = jest.fn();
        mockValidate = jest.fn();
    
        jest.mock('../../../../request/someRequest', () => ({
            getUserId: mockGetUserId,
        }));
        jest.mock('../../../../validator/formValidator', () => ({
            formValidate: mockValidate,
        }));
    });
    
    afterAll(() => {
        mockGetUserId.mockReset();
        mockValidate.mockReset();
    });
    
    it('should trigger request with test string when click button', () => {
        const {usePageExample} = require('../usePageExample');
        const {result} = renderHook(() => usePageExample());
    
        act(() => {
            result.current.onClick();
        });
    
        expect(mockGetUserId).toBeCalled();
    });
    
    it('should validate form values before submit', () => {
        const {usePageExample} = require('../usePageExample');
        const {result} = renderHook(() => usePageExample());
        const formValues = {id: '1', name: 'name'};
    
        act(() => {
            result.current.onSubmit(formValues);
        });
    
        expect(mockValidate).toBeCalledWith(formValues);
    });
    
    });
    
    // usePageExample.ts
    import {getUserId} from "../../../request/someRequest";
    import {formValidate} from "../../../validator/formValidator";
    
    export interface IFormValues {
        email: string;
        name: string;
    }
    
    export const usePageExample = () => {
        const onClick = () => {
            getUserId();
        };
        const onSubmit = (formValues: IFormValues) => {
            formValidate(formValues);
        };
    
        return {onClick, onSubmit};
    };
    
    // PageExample.tsx
    import * as React from "react";
    import {usePageExample} from "./hooks/usePageExample";
    
    export const PageExample: React.FC<IPageExampleProps> = () => {
        const {onClick, onSubmit} = usePageExample();
    
        return (
            <div>
                <form onSubmit={() => onSubmit}>
                    <input type="text"/>
                </form>
                <button onClick={onClick}>test</button>
            </div>
        );
    };

这篇文章算是给大家提供了一个hooks的TDD思路,当然其中还有一些我们也觉得不是很完善的地方(比如UI的测试),大家如果有更好的实践的话欢迎一起讨论。

本文首发于我的个人博客: https://teobler.com, 转载请注明出处  
站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

链接: http://www.fly63.com/article/detial/7490

React Hooks 你真的用对了吗?

从 React Hooks 正式发布到现在,我一直在项目使用它。但是,在使用 Hooks 的过程中,我也进入了一些误区,导致写出来的代码隐藏 bug 并且难以维护。这篇文章中,我会具体分析这些问题,并总结一些好的实践,以供大家参考

React-Hooks

以下是上一代标准写法类组件的缺点,也正是hook要解决的问题,型组件很难拆分和重构,也很难测试。业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。

精通React今年最劲爆的新特性——React Hooks

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?你在还在为组件中的this指向而晕头转向吗?这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。

如何用 Hooks 来实现 React Class Component 写法?

Hooks 的 API 可以参照 React 官网。本文主要是结合 Demo 详细讲解如何用 Hooks 来实现 React Class Component 写法,让大家更深的理解 Hooks 的机制并且更快的入门。 注意:Rax 的写法和 React 是一致的

React Hooks 底层解析[译]

对于 React 16.7 中新的 hooks 系统在社区中引起的骚动,我们都有所耳闻了。人们纷纷动手尝试,并为之兴奋不已。一想到 hooks 时它们似乎是某种魔法,React 以某种甚至不用暴露其实例

用 React Hooks 做一个搜索栏

以下是我们将要构建的搜索框的动图。这是一个简单的搜索框,我们可以用它来搜索联系人列表。我们将使用函数式组件,而不是基于类的组件来实现它。

React Hooks实践

9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。

useEffect Hook 是如何工作的?

使用useEffect 就像瑞士军刀。它可以用于很多事情,从设置订阅到创建和清理计时器,再到更改ref的值。与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。

react hooks系列之useRef

react hooks是 react 16.8 引入的特性,这里我们通过对react-hook-form进行分析来了解成熟的库是如何使用hook的。这将是一个系列,首先推荐 useRef

React将引入Hooks,你怎么看?

近日,据 MIT Technology Review 报道,一位名为“Repairnator”的机器人在 GitHub 上“卧底”数月,查找错误并编写和提交修复补丁,结果有多个补丁成功通过并被采纳,这位 Repairnator 到底是如何拯救程序员于水火的呢?

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!