こんにちは!Reactの海へようこそ。チーフアーキテクトの私です。
日々の開発、本当にお疲れ様です。
「画面のボタンを押したら数字が増える」「お買い物のカートに商品が入る」……こうした動的な機能を作れるようになると、Reactって本当に魔法のようにおもしろいですよね。
でも、少しコードが複雑になってくると、こんな不安が頭をもたげてきませんか?
- 「あれ、このカスタムフック、本当に正しく動いてる……?」
- 「状態の更新がうまくいかなくて、画面に変な数字が表示されるんだけど!」
- 「テストコードって聞くだけで、なんだか難しそう……」
大丈夫ですよ、安心してください。 最初はみんなそこでつまずきます。私も昔は、自分が書いたコードが意図通りに動いているか不安で、コンソールに `console.log` を何十個も並べて夜な夜な泣いていたものです(笑)。
今日は、そんなあなたの不安をスッキリ解消するために、React Testing Library(RTL)を使った「状態管理ロジックのテスト」について、身近な例えを交えながら優しく紐解いていきましょう。
—
そもそも、なぜテストを書く必要があるの?
テストというと、「バグを見つけるための厳しい検品作業」のようなイメージがあるかもしれません。でも、フロントエンドの現場にいる私から見ると、テストの本質は「未来の自分へのラブレター」であり、「安心してコードをリファクタリングするためのセーフティネット」です。
例えば、お気に入りの洋服ダンスを想像してみてください。
引き出しがきちんと閉まるか、服がしっかり収納できるか、新しい服を買う前に確かめたくなりますよね。プログラムの「状態管理(データを保持して更新する仕組み)」もこれと全く同じです。
- ボタンを押したら、本当に数値は「1」増える?
- マイナスにならないようにストッパーはかかっている?
これを人間の目で毎回ポチポチ確認するのは大変だし、見落としも起きます。だからこそ、「テストロボット」に自動でチェックしてもらうのが、モダンなWeb開発のスタンダードなのです。
—
今回の主役:「お買い物かご」で考えてみよう
今回は、みなさんがイメージしやすいように、「お買い物かごの個数を管理するカスタムフック(`useCounter`)」を題材にしてみましょう。
- 初期値は「0」個。
- 「増やす」ボタンを押すと、1個増える。
- 「減らす」ボタンを押すと、1個減る(ただし0未満にはならない!ここがポイント)。
このロジックがちゃんと動くか、テストで証明してみますよ。
1. テストするカスタムフックのコード
まずは、テスト対象となるフックを用意します。コンポーネントからロジックを綺麗に切り出した、いわゆる「カスタムフック」というやつです。
import { useState } from ‘react’;
// お買い物かごの個数を管理するカスタムフック
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// カウントを増やす関数
const increment = () => {
setCount((prev) => prev + 1);
};
// カウントを減らす関数(0未満にはさせない鉄壁のガード!)
const decrement = () => {
setCount((prev) => (prev > 0 ? prev – 1 : 0));
};
return { count, increment, decrement };
}
「おっ、綺麗なコードだね」と思っていただけましたか?
さて、この子が本当に「0未満にならないか」「ちゃんと増えるか」を、テストコードでスッキリ証明していきましょう。
—
React Testing Libraryでカスタムフックをテストする
React Testing Libraryというと、「画面(DOM)のテストをするもの」というイメージが強いかもしれませんが、実はカスタムフック専用の便利なヘルパー(テスト用の小さな道具)が用意されています。それが `@testing-library/react` の中にある `renderHook` という関数です。
難しく考えず、「テスト用の小さなステージに、このフックをポンと置いて動作確認する道具」だと思ってください。
2. 実際のテストコードを見てみよう
以下のコードが、今回私たちが書くテストの全貌です。エディタにそのまま貼り付けて動かせるように、日本語のコメントをたっぷり添えておきました。
import { renderHook, act } from ‘@testing-library/react’;
import { useCounter } from ‘./useCounter’;
describe(‘useCounter カスタムフックのテスト’, () => {
// テストケース1:初期値が正しく設定されるか
test(‘初期値が正しく設定されていること(デフォルトは0)’, () => {
// renderHookを使って、フックをテスト環境上で動かします
const { result } = renderHook(() => useCounter());
// 最初は「0」であることを期待する(expect)
expect(result.current.count).toBe(0);
});
// テストケース2:増やす処理のテスト
test(‘increment関数を呼ぶと、カウントが1増えること’, () => {
const { result } = renderHook(() => useCounter(5)); // 初期値を5にしてみる
// Reactの状態を更新する時は「act」という魔法の箱で囲みます
act(() => {
result.current.increment();
});
// 5から1増えて「6」になっているはず!
expect(result.current.count).toBe(6);
});
// テストケース3:減らす処理 & 0未満にならないかのガードのテスト
test(‘decrement関数を呼ぶとカウントが減るが、0未満にはならないこと’, () => {
const { result } = renderHook(() => useCounter(1)); // 初期値は1
// 1回減らす
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(0); // ここは「0」になる
// もう1回減らしてみる!(0なのにさらに減らそうとする意地悪なテスト)
act(() => {
result.current.decrement();
});
// 0未満にはならず、「0」のままで踏みとどまるはず!
expect(result.current.count).toBe(0);
});
});
—
ここでちょっと一息:「`act()`」ってなに?つまずきやすいポイント
テストコードの中で、見慣れない `act(() => { … })` という書き物が出てきましたよね。
ここ、Reactのテストで初心者の人が一番最初に頭を悩ませるポイントなんです。
`act` という名の「安全な舞台裏」
Reactの状態更新(`useState` など)は、非同期で行われたり、画面の再描画(レンダリング)のスケジュールと密接に関わっています。
テストの世界は現実世界よりもずっと時間が進むのが早いため、「おいおい、状態を更新したから、その結果が綺麗に反映されるまでちょっと待って、全てを安全に処理しきってくれよ!」とReactにお願いしてあげる必要があります。
それが `act()` の役割です。
「状態を変化させる操作(ボタンクリックや関数の実行など)を行うときは、必ず `act()` で包み込んであげる」
これだけ覚えておけば、テスト中の非同期バグや「なんか状態が反映されてないぞ?」というトラブルの9割は防げます。大丈夫、おまじないだと思ってまずは使ってみてくださいね。
—
まとめ:テストはあなたの最強の味方
今回は、React Testing Libraryを使ったカスタムフックのテスト戦略について、お買い物かごの例えを交えてお話ししました。
- テストの本質は「未来の自分へのラブレター」であり、コードの安全性を守る盾であること。
- カスタムフックのテストには `renderHook` がめちゃくちゃ便利であること。
- 状態を動かすときは `act()` というおまじないで包んであげること。
最初は「覚えることが多くて面倒だな」と感じるかもしれません。でも、自分で書いたテストコードが緑色の「PASS(成功)」の文字を出してくれた瞬間、なんとも言えない達成感と、「俺のコード、完璧に動いてるぞ!」という絶大な安心感が得られます。
焦らず、一歩ずつあなたのペースで進んでいきましょう。
あなたのReactライフが、より楽しく、自信に満ちたものになりますように。チーフアーキテクトの私は、いつもあなたの挑戦を応援しています!

コメント