由于篇幅所限文章中并没有给出demo的所有代码,大家如果有兴趣可以将代码clone到本地从commit来看整个demo的TDD过程,配合文章来看会比较清晰。本文涉及的所有代码地址: teobler/TDD-with-React-hooks-demo
从进公司前认识了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();
};
这类组件我们没有一个个去测试组件里面的元素,而是按照UX的要求build完组件以后加上一个jest的json snapshot测试。
注意这里的snapshot并不是大家印象中的e2e测试中的截图,而是jest里将组件render出来之后使用json生成一份UI的dom结构,在下次测试时,生成一份新的快照与旧的快照进行比对,从而得出两个UI不一样的地方,实现对UI的保护。
但是其实使用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>
);
};
这个部分我们就需要hooks的帮忙了,这样的组件不是UI和逻辑强耦合嘛,那我们就可以将两者拆开。于是这样的组件我们会这样写:
// 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, 转载请注明出处
近日,据 MIT Technology Review 报道,一位名为“Repairnator”的机器人在 GitHub 上“卧底”数月,查找错误并编写和提交修复补丁,结果有多个补丁成功通过并被采纳,这位 Repairnator 到底是如何拯救程序员于水火的呢?
你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?你在还在为组件中的this指向而晕头转向吗?这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。
我们将userReducer函数返回的原始dispath命名为origin_dispatch,自定义dispatch函数,当action为函数的时候,我们执行action函数,并将origin_dispatch当作参数传进去;action不是函数,直接调用origin_dispatch,不做处理
使用useEffect 就像瑞士军刀。它可以用于很多事情,从设置订阅到创建和清理计时器,再到更改ref的值。与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。
从 React Hooks 正式发布到现在,我一直在项目使用它。但是,在使用 Hooks 的过程中,我也进入了一些误区,导致写出来的代码隐藏 bug 并且难以维护。这篇文章中,我会具体分析这些问题,并总结一些好的实践,以供大家参考
Hooks 的 API 可以参照 React 官网。本文主要是结合 Demo 详细讲解如何用 Hooks 来实现 React Class Component 写法,让大家更深的理解 Hooks 的机制并且更快的入门。 注意:Rax 的写法和 React 是一致的
以下是上一代标准写法类组件的缺点,也正是hook要解决的问题,型组件很难拆分和重构,也很难测试。业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
Hooks出来已经有段时间了,相信大家都用过段时间了,有没有小伙伴们遇到坑呢,我这边就有个 setInterval 的坑,和小伙伴们分享下解决方案。写个 count 每秒自增的定时器,如下写法结果,界面上 count 为 1 ?
对于 React 16.7 中新的 hooks 系统在社区中引起的骚动,我们都有所耳闻了。人们纷纷动手尝试,并为之兴奋不已。一想到 hooks 时它们似乎是某种魔法,React 以某种甚至不用暴露其实例
9月份开始,使用了React16.8的新特性React Hooks对项目进行了重构,果然,感觉没有被辜负,就像阮一峰老师所说的一样,这个 API 是 React 的未来。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!