試著使用自定義鉤子進行測試
首先
当使用自定义 Hook 在测试 React 时,我觉得可以将逻辑部分分离出来,这样更容易进行小规模测试,这件事看起来非常麻烦。所以我决定尝试一下。
本次将使用最常见的计数器演示和实际开发中经常使用的输入示例进行讲解。
我将使用testing-library/react和testing-library/react-hooks来编写测试。
GitHub → GitHub 仓库
这是最终成果物仓库。
柜台的例子
现在马上开始编写计数器的演示。
这是一个简单的示例,当按下按钮时,状态的值增加 1。
如果在React组件中直接写入的情况下。
export const Counter: React.FC = () => {
const { count, setCount } = useState(0);
const handleIncrement = useCallback(() => {
setCount((prevCount) => prevCount + 1);
},[]);
return (
<div>
<p>count: {count}</p>
<button onClick={handleIncrement}>add</button>
</div>
);
};
将Custom Hook分离出来
type ReturnType = {
count: number;
handleIncrement: () => void;
};
export const useCounter = (): ReturnType => {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return {
count,
handleIncrement,
};
};
export const Counter: React.FC = () => {
const { count, handleIncrement } = useCounter();
return (
<div>
<p>count: {count}</p>
<button onClick={handleIncrement}>add</button>
</div>
);
};
我在 useCounter.ts 中定义了 count 变量和 handleIncrement 函数。
通过在 Counter.tsx 中调用它们,我可以使用 count 和 handleIncrement 来进行渲染和执行计数。
Custom Hooks的测试
import { act, RenderResult, renderHook } from "@testing-library/react-hooks";
import { useCounter, ReturnType } from "../hooks/useCounter";
describe("useCounter", () => {
let result: RenderResult<ReturnType>;
beforeEach(() => {
result = renderHook(() => useCounter()).result;
});
it("countの初期値は0である", () => {
expect(result.current.count).toBe(0);
});
it("handleIncrementを1度呼んだ後、countの値は1である", () => {
act(() => {
result.current.handleIncrement();
});
expect(result.current.count).toBe(1);
});
});
我正在使用 react-testing-library 实现测试。
通过使用 react-testing-library 的 renderHook 函数,可以测试 hooks。非常方便。
需要在act函数中调用类似handleIncrement这样的函数来更新状态。
输入文本的示例
下面将为react中常用的input标签的自定义Hooks示例编写代码。
鉴于这么好的机会,我们将处理两个状态:名字(firstName)和姓氏(lastName)。
此外,我们还将添加一个简单的验证,限制firstName不超过8个字符,lastName不超过6个字符。
通过input标签从用户接收输入的字符串。
在React组件中直接编写
export const TextInput: React.FC = () => {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const handleChangeFirstName = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
// バリデーション 8文字まで
if (firstName.length >= 8) {
return;
}
setFirstName(ev.target.value);
},
[firstName]
);
const handleChangeLastName = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
// バリデーション 6文字まで
if (lastName.length >= 6) {
return;
}
setLastName(ev.target.value);
},
[lastName]
);
return (
<div>
<input type="text" value={firstName} onChange={handleChangeFirstName} />
<input type="text" value={lastName} onChange={handleChangeLastName} />
</div>
);
};
如果状态增加到三个或四个,写起来会非常困难,而且验证部分如果都要手动编写的话会变得很繁琐。
将Custom Hook分离
export type ReturnType = [
string,
(ev: React.ChangeEvent<HTMLInputElement>) => void
];
export const useTextInput = (maxLength: number): ReturnType => {
const [value, setValue] = useState("");
const handleChange = useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
if (value.length >= maxLength) {
return;
}
setValue(ev.target.value);
},
[value, maxLength]
);
return [value, handleChange];
};
export const TextInput: React.FC = () => {
const [firstName, handleChangeFirstName] = useTextInput(8);
const [lastName, handleChangeLastName] = useTextInput(6);
return (
<div>
<input type="text" value={firstName} onChange={handleChangeFirstName} />
<input type="text" value={lastName} onChange={handleChangeLastName} />
</div>
);
};
在 useCounter.ts 中定义了 value 变量和 handleChange 函数。
在 React 组件中的代码量大大减少。
另外,将 useTextInput 的参数设置为相应的最大输入字符数。
函数的返回值
在`useCounter.ts`和`useTextInput`中,返回的形式不同。如果以对象的形式返回,那么在调用时只能使用基本相同的变量名和函数名来调用。
const { count, handleIncrement } = useCounter();
一种方法是将其放入数组并返回,这样在调用时可以给它取另一个名称。
const [ name, handleChangeName ] = useTextInput();
const [ counter, setCounter] = useState<number>(0);
const [ name, setName ] = useState<string>("");
在前一种情况下,您可以只调用您需要的内容,而后者则需要按顺序调用相应的内容。例如,如果您只想调用 useTextInput ,而不是 handleChangeName ,可以按照以下方式操作。
const [ , handleChangeName ] = useTextInput();
测试自定义钩子
describe("UseTextInput", () => {
let result: RenderResult<ReturnType>;
beforeEach(() => {
result = renderHook(() => useTextInput(10)).result;
});
test("初期値は空文字である", () => {
const [value] = result.current;
expect(value).toBe("");
});
test("入力値が反映される", () => {
const { container } = render(<input type="text" {...[result.current]} />);
const input = container.querySelector("input");
fireEvent.change(input!, { target: { value: "test" } });
expect(input!.value).toBe("test");
});
});
在 useTextInput.spec.tsx 文件中,我们将与 DOM 测试相结合来测试自定义 Hook。我们创建了一个用于测试的输入框,并将通过 useTextInput 创建的值和函数传递给它。
如果对这种做法有更好的方法的话,请务必分享您的意见!这种方法不好!
最後
作为今后想要采取的设计方针,大致如下。
-
- react コンポーネントから Custom Hook に分離する
-
- Custom Hook のユニットテストはどんどんやっていく
- DOM のユニットテストはリソースと相談しつつ頑張っていきます
我会努力与测试友好合作,编写安全的程序。