前面,我们在
Counter
组件中使用
useState 计数,并根据条件显示警告信息。
不过,让我们重新看看组件代码。
绘制界面的代码(JSX)和计算数字的代码(Logic)混在同一个地方。
如果其他页面也需要这个“计数”功能,该怎么办?
要复制粘贴代码吗?
这时就可以使用
自定义 Hook(Custom Hook)
。
今天,我们将逻辑作为大脑(Hook)、界面作为身体(Component)分离,并学习如何单独测试这个大脑。
第 1 步:分离逻辑(Refactoring) 🔗
首先创建
src/hooks 文件夹,将组件中的逻辑移到名为
useCounter.ts 的文件中。
这个过程不修改测试代码,只改变内部结构,属于*重构(Refactoring)*阶段。
src/hooks/useCounter.tsimport { useState } from 'react';
// [로직] 숫자를 관리하고 증가시키는 기능만 담당합니다.
export default function useCounter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prev) => prev + 1);
};
// 컴포넌트에서 필요한 값과 함수를 반환합니다.
return {
count,
increment,
};
}
现在,Counter.tsx 不再直接计算,只需使用刚才创建的 Hook。
src/components/Counter.tsximport CountButton from './CountButton';
import useCounter from '../hooks/useCounter'; // Hook import
export default function Counter() {
// [수정] 직접 useState를 쓰지 않고 Hook을 사용합니다.
const { count, increment } = useCounter();
return (
<div>
<p>현재 숫자: {count}</p>
{/* 함수 이름이 handleClick에서 increment로 바뀌었음에 주의하세요 */}
<CountButton label="+" onClick={increment} />
{count >= 3 && (
<p style={{ color: 'red' }}>
숫자가 너무 커요!
</p>
)}
</div>
);
}
第 2 步:编写 Hook 测试(renderHook 与 act) 🔗
接下来,需要测试抽离出来的
useCounter 是否正常工作。
不过,Hook 不是组件,不能像
render(<useCounter />) 那样使用。
这时需要的工具就是
renderHook 和
act。
renderHook
创建一个虚拟组件,并在其中运行 Hook。
act
告诉 React
“现在要执行改变状态的重要操作”的函数。模拟点击事件等操作时使用。
创建
src/hooks/useCounter.test.ts 文件,写入以下代码。
这次也通过日志追踪状态变化。
src/hooks/useCounter.test.tsimport { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
import { describe, test, expect } from 'vitest';
describe('useCounter Hook 동작 테스트', () => {
test('초기값은 0이며, increment 함수를 호출하면 값이 1 증가한다', () => {
// 1. Hook 렌더링 (가상 환경)
const { result } = renderHook(() => useCounter());
console.log(`[Step 1] 초기값 확인: ${result.current.count}`);
// 초기값 검증
expect(result.current.count).toBe(0);
// 2. 상태 변경 (act 사용 필수!)
console.log('[Step 2] increment 함수 실행');
act(() => {
result.current.increment();
});
console.log(`[Step 3] 변경된 값 확인: ${result.current.count}`);
// 3. 결과 검증
expect(result.current.count).toBe(1);
});
});
现在运行 Hook 测试。
Terminal

查看运行日志
[Step 1] 초기값 확인: 0
[Step 2] increment 함수 실행
[Step 3] 변경된 값 확인: 1
PASS src/hooks/useCounter.test.ts
PASS src/components/Counter.test.tsx
从日志可以看到,在 act 函数中调用 increment 后,result.current.count 的值立即从 0 变为 1。
还请注意,原来的
Counter.test.tsx 依然
PASS
。
虽然我们彻底修改了内部实现(useState → 自定义 Hook),但用户看到的界面与行为完全相同,所以原来的测试并未失效,仍能正常通过。
Terminalgit clone https://github.com/PROMLEE/my-tdd-app.git
cd my-tdd-app
git checkout part7
今天学习了如何分离逻辑并独立测试。
renderHook
无需组件,也能单独运行、验证 Hook 的工具。
act
所有改变状态(State)的操作都应包裹在 act 函数中,让 React
检测到变化并反映更新。
关注点分离
组件专注于“展示”,Hook 专注于“计算”,代码因此更加清晰。
现在,我们的应用内部逻辑(Hook)与外部界面(Component)都由测试来保障。
下一篇将介绍实际 Web 应用的核心:
第 8 篇,异步逻辑测试
。