前言
在上一篇中我们补充了一些对react component常见的一些能力做的单测,这一篇我们在上一篇的基础上继续查漏补缺,保证我们的单测能够覆盖到我们的场景
判断方法调用
在react component中,方法无疑是非常重要的一类,在日常的开发中,为了让组件能够提高复用性或者解耦,我们会基于某些阶段或者场景对外暴露一些生命周期hook方法或者提供callback之类的,我们的单测也是要保证这些方法能够正常的被调用到
修改组件
这里增加了一个onClickTitle
参数,传入一个函数,当点击标题的时候把标题内容作为函数的入参返回
import "./index.css";
import {
PropsWithChildren,
ReactNode,
CSSProperties,
useCallback,
} from "react";
interface TodoHeaderProps {
// 待办事项的标题
title: string;
// 最外层容器的样式
containerStyle?: CSSProperties;
// 是否结束
isFinish?: boolean;
// 图标的链接
iconUrl?: string;
// 额外的信息
extraInfo?: ReactNode;
// 点击标题的事件
onClickTitle?: (title: string) => void;
}
export default function TodoHeader({
title,
containerStyle,
iconUrl,
isFinish = false,
children,
extraInfo,
onClickTitle,
}: PropsWithChildren<TodoHeaderProps>) {
// 点击标题的方法
const clickTitleFn = useCallback(() => {
onClickTitle?.(title);
}, [onClickTitle, title]);
return (
<div className="report-header" style={containerStyle}>
{iconUrl && <img src={iconUrl} alt="icon" />}
<span
className="title"
data-testid="todo-header-title"
style={{ background: isFinish ? "red" : "white" }}
onClick={clickTitleFn}
>
{title}
</span>
<span className="extra">{extraInfo}</span>
{children}
</div>
);
}
修改一下调用
const onClickTitle = useCallback((title: string) => {
window.alert(title);
}, []);
<TodoHeader
title="这是一个标题"
containerStyle={{ border: "1px solid blue" }}
isFinish={true}
iconUrl={logo}
onClickTitle={onClickTitle}
/>
看一下效果,符合预期
单测编写
这里我们需要用到几个知识点,一个是mock函数,通过jest.fn()
我们创建了一个用于测试的函数,传入到组件中,第二个是toBeCalled
这个api,通过这个我们可以来断言我们的方法是否有被调用到。第三个是fireEvent
,这个是testing libray
提供的一个模拟用户操作的一个功能,比如我们这里用到的一个click
事件
it(`正确响应onClickTitle的事件`, () => {
const mockClickFn = jest.fn();
const title = "标题";
const { getByText } = render(
<TodoHeader title={title} onClickTitle={mockClickFn} />
);
fireEvent.click(getByText(title));
expect(mockClickFn).toBeCalled();
});
我们运行一下这个用例看看效果pnpm test src/components/__tests__/todo-header.test.tsx
这里还可以介绍另外toBeCalledTimes
这个api,可以断言被调用的次数,我们在上面的的用例的基础上加一个断言,断言调用了两次,当然这里实际上只会调用一次,这里会不通过
it(`正确响应onClickTitle的事件`, () => {
const mockClickFn = jest.fn();
const title = "标题";
const { getByText } = render(
<TodoHeader title={title} onClickTitle={mockClickFn} />
);
fireEvent.click(getByText(title));
expect(mockClickFn).toBeCalled();
expect(mockClickFn).toBeCalledTimes(2);
});
运行结果如下,
我们把上面的
toBeCalledTimes
改为1次,重新运行一下,现在就可以通过了
再补充一个api,
toBeCalledWith
,这个可以断言一些调用的参数,我们在上面的case再加一个断言看看
expect(mockClickFn).toBeCalledWith(title);
重新运行下还是通过的
判断异步方法的调用
我们知道,在js中充满着各种异步,我们在此之前碰到过的各种单测基本上都没有涉及异步的东西,但是在实际项目中异步是必不可少的,所以这里我们也做一下异步这种情况的单测
修改组件
我们在原先的组件中增加多一个onInit
的入参,这个入参是一个异步的函数,返回一个字符串,在组件中我们调用这个方法,然后通过setState
的方法将返回的字符串设置为标题,达到异步更新title的效果
import "./index.css";
import {
PropsWithChildren,
ReactNode,
CSSProperties,
useCallback,
useState,
useEffect,
} from "react";
interface TodoHeaderProps {
// 待办事项的标题
title: string;
// 最外层容器的样式
containerStyle?: CSSProperties;
// 是否结束
isFinish?: boolean;
// 图标的链接
iconUrl?: string;
// 额外的信息
extraInfo?: ReactNode;
// 点击标题的事件
onClickTitle?: (title: string) => void;
// 初始化的方法
onInit?: () => Promise<string>;
}
export default function TodoHeader({
title,
containerStyle,
iconUrl,
isFinish = false,
children,
extraInfo,
onClickTitle,
onInit,
}: PropsWithChildren<TodoHeaderProps>) {
const [currentTitle, setCurrentTitle] = useState<string>(title);
// 点击标题的方法
const clickTitleFn = useCallback(() => {
onClickTitle?.(title);
}, [onClickTitle, title]);
useEffect(() => {
if (onInit) {
(async () => {
const result = await onInit();
setCurrentTitle(result);
})();
}
}, [onInit]);
return (
<div className="report-header" style={containerStyle}>
{iconUrl && <img src={iconUrl} alt="icon" />}
<span
className="title"
data-testid="todo-header-title"
style={{ background: isFinish ? "red" : "white" }}
onClick={clickTitleFn}
>
{currentTitle}
</span>
<span className="extra">{extraInfo}</span>
{children}
</div>
);
}
单测编写
首先我们先按我们之前的思路来写单测 ,mock一个异步函数,返回一个新的字符串,然后查询这个字符串是否存在于视图中
it(`正确响应onInit事件`, () => {
const title = "标题";
const newTitle = "新的标题";
const mockInitFn = jest.fn(() => Promise.resolve(newTitle));
const { queryByText } = render(
<TodoHeader title={title} onInit={mockInitFn} />
);
const element = queryByText(newTitle);
expect(element).not.toBeNull();
});
看下运行结果,果不其然已经报错了
这时候我们可以看到报错提示中已经非常明显的提示我们要怎么做,提示大概的意思就是因为我们内部有update state,然后这个update state实际上是异步的,我们做的断言是基于update之后的内容,所以解决方案我们就是需要用
act
包一下这个render方法
改一下上面的测试用例
it(`正确响应onInit事件`, async () => {
const title = "标题";
const newTitle = "新的标题";
const mockInitFn = jest.fn(() => Promise.resolve(newTitle));
await act(async () => {
render(<TodoHeader title={title} onInit={mockInitFn} />);
});
const element = screen.queryByText(newTitle);
expect(element).not.toBeNull();
});
看下运行的结果,这回是已经通过了
另外的解决方案,实际上我们在之前的用例中实际上也是有用过,就是用的
waitFor
这个api
我们同样基于上面的case改写一下,其实就是将act
改为了waitFor
,其他都不变
it(`正确响应onInit事件`, async () => {
const title = "标题";
const newTitle = "新的标题";
const mockInitFn = jest.fn(() => Promise.resolve(newTitle));
await waitFor(async () => {
render(<TodoHeader title={title} onInit={mockInitFn} />);
});
const element = screen.queryByText(newTitle);
expect(element).not.toBeNull();
});
运行结果
fireEvent简单使用
在上面的case中我们已经使用过了fireEvent,这是testing library提供给我们模拟一些用户行为的api,常见比如各种点击,各种用户交互的事件等等,上面我们已经尝试过fireEvent.click这种情况了,现在我们补充多一种场景
修改组件
我们增加多一个Input,然后当这个Input修改的时候我们也update state修改这个title
import "./index.css";
import {
PropsWithChildren,
ReactNode,
CSSProperties,
useCallback,
useState,
useEffect,
} from "react";
import { Input } from "antd";
interface TodoHeaderProps {
// 待办事项的标题
title: string;
// 最外层容器的样式
containerStyle?: CSSProperties;
// 是否结束
isFinish?: boolean;
// 图标的链接
iconUrl?: string;
// 额外的信息
extraInfo?: ReactNode;
// 点击标题的事件
onClickTitle?: (title: string) => void;
// 初始化的方法
onInit?: () => Promise<string>;
}
export default function TodoHeader({
title,
containerStyle,
iconUrl,
isFinish = false,
children,
extraInfo,
onClickTitle,
onInit,
}: PropsWithChildren<TodoHeaderProps>) {
const [currentTitle, setCurrentTitle] = useState<string>(title);
// 点击标题的方法
const clickTitleFn = useCallback(() => {
onClickTitle?.(title);
}, [onClickTitle, title]);
useEffect(() => {
if (onInit) {
(async () => {
const result = await onInit();
setCurrentTitle(result);
})();
}
}, [onInit]);
return (
<div className="report-header" style={containerStyle}>
{iconUrl && <img src={iconUrl} alt="icon" />}
<span
className="title"
data-testid="todo-header-title"
style={{ background: isFinish ? "red" : "white" }}
onClick={clickTitleFn}
>
{currentTitle}
</span>
<Input
type="text"
style={{ width: 300, display: "flex" }}
value={currentTitle}
onChange={e => setCurrentTitle(e.target.value)}
/>
<span className="extra">{extraInfo}</span>
{children}
</div>
);
}
单测编写
这里会用到上面讲到的act
,获取到对应的Input之后,我们通过fireEvent.change
来设置改变后的值,注意参数格式
it(`正确处理Input change 事件`, async () => {
const title = "标题";
const newTitle = "新的标题";
const { container } = render(<TodoHeader title={title} />);
const inputElement = container.querySelector("input");
expect(inputElement).not.toBeNull();
await act(async () => {
fireEvent.change(inputElement!, { target: { value: newTitle } });
});
const element = screen.queryByText(newTitle);
expect(element).not.toBeNull();
});
结尾
本文在上一篇blog的基础上补充了两种情况,并且针对场景的场景我们使用了fireEvent来模拟触发一些操作后调用的事件
本章节代码可参考:
github.com/liyixun/rea…
github.com/liyixun/rea…
传送门
前端单测学习(1)—— 单测入门之react单测项目初步
前端单测学习(2)—— react 组件单测初步
前端单测学习(3)—— react组件单测进阶
前端单测学习(4)—— react 组件方法&fireEvent
前端单测学习(5)—— 快照
前端单测学习(6)—— 定时器
前端单测学习(7)—— mock
前端单测学习(8)—— react hook
前端单测学习(9)—— 覆盖率报告
前端单测学习(10)—— 状态管理redux
前端单测学习(11)—— react hook 进阶
前端单测学习(12)—— 性能优化
前端单测学习(13)—— 自动化测试