使用 React、Typescript 和 react-testing-library 编写单元测试
我所在的公司开始采用 TypeScript 作为编写 React 的首选解决方案。在代码审查期间,我注意到很多人在测试组件时遇到了问题。在查看代码时,我注意到它的编写方式让 TypeScript 看起来更像是一个负担,而不是一个辅助你编写代码的工具。
凭借一些使用 Typescript 的经验,我想到了一种编写测试的模式,在我看来,这种模式可以避免不必要的重复并使其清晰。
示例组件
jest
这是我们要测试的组件。它非常简单,但包含足够的逻辑,以便我们可以使用和 的一些功能react-testing-library
。
import React from "react";
import { Todo } from "./Todo";
type Props = {
id: number;
onClick: (todo: Todo) => void;
};
type State = {
fetchState: "loading" | "error" | "success";
todo: Todo | undefined;
};
function Todo({ id, onClick }: Props) {
const [state, setState] = React.useState<State>({
fetchState: "loading",
todo: undefined
});
React.useEffect(() => {
function fetchTodo() {
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then<Todo>(response => response.json())
// Normally we would probably check if the component
// is still mounted here, before using `setState`
.then(todo => setState({ todo, fetchState: "success" }))
.catch(() => setState({ todo: undefined, fetchState: "error" }));
}
fetchTodo();
}, [id]);
if (state.fetchState == "loading" || !state.todo) return <p>loading ...</p>;
if (state.fetchState == "error") return <p>error!...</p>;
return (
<div onClick={() => onClick(state.todo as Todo)}>
<p>{state.todo.title}</p>
<p>{state.todo.id}</p>
</div>
);
}
就像我说的,这里的代码其实并不重要。它只是为了让我们有东西可以测试。
测试
您的测试用例可能看起来像这样:
import { render } from "@testing-library/react";
it("fetches a todo", () => {
const {/* selectors */} = render(<Todo onClick={() => {}} id={1} />);
// rest of the test
});
it("handles non-existing id", () => {
const {/* selectors */} = render(<Todo onClick={() => {}} id={420} />);
// rest of the test
});
// more test cases
这没有什么不对。
但是,当你编写第四、第五个测试用例时,你可能会厌倦所有这些重复。注意到我必须明确提供onClick
函数,即使该函数在测试中不会用到(例如handles non-existing id
)?
renderUI
我们可以通过创建或函数来消除所有这些重复setup
(这些只是命题,随便你怎么称呼它)。
renderUI
功能
让我们创建renderUI
负责渲染组件并返回react-testing-library
选择器和实用程序的函数。
function renderUI(props: ?) {
return render(<Todo {...props}/>)
}
现在,我故意在这里留下了问号。你可能想直接导入props
from的类型./App
(也就是包含我们要测试的组件的文件)。
import { render } from "@testing-library/react";
import { Todo, Props } from "./App";
function renderUI(props: Props) {
return render(<Todo {...props} />);
}
虽然你当然可以这样做,但我个人不建议这样做。
-
除非您使用像这样的详细名称
TodoComponentProps
,否则导出组件道具的类型可能会与其他导出的类型发生冲突,这在使用代码完成时尤其痛苦。 -
导出组件 props 的类型可能会让以后阅读代码的人感到困惑。我可以更改类型的名称吗?这些名称在其他地方有用吗?
考虑到这一点,让我们利用 Typescript 功能并获取组件道具的类型,而无需导出/导入它们。
import { render } from "@testing-library/react";
import { Todo } from "./App";
type ComponentProps = React.ComponentProps<typeof Todo>;
function renderUI(props: ComponentProps) {
return render(<Todo {...props} />);
}
我使用React.ComponentProps
内部定义的泛型@types/react
来获取所需的类型。无需导出/导入 props 类型!
这样,在我们的测试中,我们摆脱了一些重复:
it("fetches a todo", () => {
const { /* selectors */ } = renderUI({ onClick: () => {}, id: 1 });
// rest of the test
});
但是,我们仍然必须包含对于给定测试用例(onClick
在本例中)并不真正重要的属性。TypescriptParial<T>
实用程序类型可以帮助实现这一点。
import { Todo } from "./App";
type ComponentProps = React.ComponentProps<typeof Todo>;
const baseProps: ComponentProps = {
onClick: () => {},
id: 1
};
function renderUI(props: Partial<ComponentProps> = {}) {
return render(<Todo {...baseProps} {...props} />);
}
请注意,我必须创建baseProps
。这些属性应该以组件能够实际使用它们进行渲染的方式指定。baseProps
和props
组合允许我们仅将这些属性传递给renderUI
在给定测试上下文中重要的函数。
it("handles non-existing id", () => {
const {/* selectors */} = render(<Todo id={420} />);
// rest of the test
});
测试用例handles non-existing id
确实测试了响应用户点击的能力,因此它没有指定onClick
函数。这是可能的,因为我们在函数baseProps
中包含了它renderUI
。
重新渲染
有时,您需要使用rerender
从react-testing-library
render
函数返回的函数来测试当给定的 prop 发生变化时(更改之前和更改后)组件的行为。
查看函数的签名rerender
:
rerender: (ui: React.ReactElement) => void;
它需要一个类型的参数React.ReactElement
。这意味着我们的renderUI
函数,就目前情况而言,不会切断它。
it("reacts to id change", () => {
const { rerender } = renderUI({ id: 1 });
// assert
rerender(<Todo {...baseProps} id={2} />);
// assert
});
我们可以rerender
按照抽象的同样的方式抽象该函数render
。
function renderUI(props: Partial<ComponentProps> = {}) {
const rtlProps = render(<Todo {...baseProps} {...props} />);
return {
...rtlProps,
rerender: (newProps: Partial<ComponentProps>) =>
rtlProps.rerender(<Todo {...baseProps} {...props} {...newProps} />)
};
}
我替换了返回的rerender
函数。它不再直接返回原始函数,而是将组件的渲染抽象出来,这使得我们的测试更加清晰。
it("reacts to id change", () => {
const { rerender } = renderUI({ id: 1 });
// assert
rerender({ id: 2 });
// assert
});
警告
我只是想指出,有时候重复并不一定是坏事。草率地创建抽象肯定比props
多次传递更糟糕。
这就是为什么我仅当您觉得有必要时才建议您遵循我在此处给出的建议。
有一篇很棒的文章,在测试中(以及一般情况下)创建任何类型的抽象之前,你一定要阅读并考虑它。
概括
总的来说,我认为这种模式可以帮助您更快地编写测试并减少重复。
请记住,我不是测试和/或 Typescript 领域的专家,因此如果您感觉有什么不对或不正确,请联系我!
你可以在 Twitter 上关注我:@wm_matuszewski
谢谢👋
文章来源:https://dev.to/wojciechmatuszewski/writing-unit-tests-with-react-typescript-and-react-testing-library-1nmg