用React hooks实现TDD

更新日期: 2020-02-18阅读: 2.1k标签: 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, 转载请注明出处  

链接: https://fly63.com/article/detial/7490

React将引入Hooks,你怎么看?

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

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

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

使用react hooks实现自己的context-redux

我们将userReducer函数返回的原始dispath命名为origin_dispatch,自定义dispatch函数,当action为函数的时候,我们执行action函数,并将origin_dispatch当作参数传进去;action不是函数,直接调用origin_dispatch,不做处理

useEffect Hook 是如何工作的?

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

React Hooks 你真的用对了吗?

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

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

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

React-Hooks

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

React Hooks与setInterval

Hooks出来已经有段时间了,相信大家都用过段时间了,有没有小伙伴们遇到坑呢,我这边就有个 setInterval 的坑,和小伙伴们分享下解决方案。写个 count 每秒自增的定时器,如下写法结果,界面上 count 为 1 ?

React Hooks 底层解析[译]

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

React Hooks实践

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

点击更多...

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