TypeScriptでReactのユニットテストに型を適用!初心者向けベストプラクティス
生徒
「Reactで作ったアプリが正しく動くか確認するテストを書きたいのですが、TypeScriptを使うとどう便利になるんですか?」
先生
「TypeScriptの型をテストに活用すると、テストコード自体が間違っているときにすぐに気づけるようになります。バグを未然に防ぐ強力な味方になりますよ。」
生徒
「なるほど!初心者でも型をうまく使ってテストを書くコツはありますか?」
先生
「はい、あります。それでは、Reactのテストで型をどう使うのか、基本的なところから順番に見ていきましょう!」
1. ユニットテストとTypeScriptの役割
まず最初に、ユニットテストという言葉について説明します。ユニットテストとは、プログラムの小さな部品(関数やコンポーネントなど)が、単体で正しく動くかどうかを確認する作業のことです。パソコンを一度も触ったことがない方でもイメージしやすいように例えると、料理を作る前に「コンロの火がちゃんとつくか」「包丁が切れるか」を一つずつ確認するようなものです。大きな料理(アプリ)を作り始めてから道具の故障に気づくと大変ですよね。だから、小さな単位で確認を行います。
ここにTypeScript(タイプスクリプト)を組み合わせることで、さらに安心感が増します。TypeScriptは「型」を決めることができる言語です。例えば、「この箱には数字しか入れてはいけません」というルールを作ることができます。テストを書くときにこの「型」を使うと、間違って文字を入れてしまった場合に、プログラムを動かす前にコンピューターが「そこは数字ですよ!」と教えてくれます。これを静的解析と呼び、開発のミスを劇的に減らすことができます。
2. Reactコンポーネントの型定義
Reactでテストを書く前に、まずはテスト対象となるコンポーネントに型を付けておく必要があります。コンポーネントとは、画面の部品のことです。例えば、ボタンや入力フォームなどがそれにあたります。コンポーネントには、外部からデータを受け取るための窓口があり、これをProps(プロップス)と呼びます。
TypeScriptでは、このPropsにどんなデータが来るのかを事前に定義します。これにより、テストコードを書くときにも「どんなデータを渡すべきか」が明確になります。まずは、名前を表示するだけの簡単なコンポーネントを作成してみましょう。
type GreetingProps = {
name: string;
};
const Greeting = ({ name }: GreetingProps) => {
return <h1>こんにちは、{name}さん!</h1>;
};
export default Greeting;
このコードでは、GreetingPropsという型を作り、nameは必ず「文字列(string)」である必要があると決めています。もしテストで数字を渡そうとすると、TypeScriptがエラーを出して守ってくれます。
3. テストライブラリでの型の活用
Reactのテストでは、主にJest(ジェスト)やReact Testing Library(リアクト・テスティング・ライブラリ)という道具を使います。これらは、実際にブラウザを動かさなくても、プログラム上で画面の動きをシミュレーションしてくれる便利なツールです。初心者のうちは、「テストを実行してくれる魔法の杖」だと思ってください。
テストコードでも、TypeScriptの力を借ります。例えば、コンポーネントを画面に表示(レンダリング)する関数を使うとき、型定義がしっかりしていれば、必要なデータが足りない場合にすぐに警告が出ます。以下のコードは、先ほどのコンポーネントが正しく表示されるかを確かめるテストの例です。
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('名前が正しく表示されること', () => {
// コンポーネントを表示させる
render(<Greeting name="太郎" />);
// 画面内に「こんにちは、太郎さん!」という文字があるか探す
const element = screen.getByText(/こんにちは、太郎さん!/);
// 見つかったら成功!
expect(element).toBeInTheDocument();
});
ここでは、renderという関数を使ってコンポーネントを仮想的な画面に映し出しています。もしnameを入れ忘れると、TypeScriptが赤線を引いてミスを教えてくれます。
4. モック(模擬データ)にも型を適用する
テストをしていると、本物のデータの代わりに「偽物のデータ」を使いたいときがあります。これをモックと呼びます。例えば、インターネットから情報を取ってくる処理がある場合、毎回本物のサイトに繋ぐと時間がかかったり、相手のサイトに迷惑がかかったりします。そこで、「データを取ってきたふり」をする偽物の関数を作ります。
この偽物の関数にも型を付けるのがベストプラクティスです。そうしないと、偽物のデータの形が本物とズレてしまい、テストの意味がなくなってしまうからです。TypeScriptのjest.MockedFunctionなどを使うと、本物と同じ形をした偽物を簡単に作ることができます。
// データを取得する関数の型を定義
type GetUser = (id: number) => { name: string; age: number };
// 偽物の関数を作成
const mockGetUser = jest.fn() as jest.MockedFunction<GetUser>;
test('ユーザー情報を取得するテスト', () => {
// 偽物の戻り値を設定
mockGetUser.mockReturnValue({ name: '花子', age: 25 });
const user = mockGetUser(1);
expect(user.name).toBe('花子');
});
このように型を指定することで、偽物のデータが「名前」と「年齢」を正しく持っていることを保証できます。初心者の方は、モックを「練習用の身代わり人形」だと考えると分かりやすいでしょう。
5. イベントハンドラのテストと型
次に、ボタンをクリックしたときなどの「動き」をテストする方法についてです。これをイベントハンドラのテストと呼びます。例えば、ボタンを押したときに何らかの関数が実行されるかどうかを確認します。ここでも型は重要です。関数がどんな引数を受け取り、何を返すのかという型が決まっていると、テストの信頼性が高まります。
以下の例では、クリックされたときに呼ばれる関数を受け取るボタンコンポーネントのテストを行います。クリックという「出来事」が正しく発生したかをチェックします。
import { render, screen, fireEvent } from '@testing-library/react';
type ButtonProps = {
onClick: () => void; // 何も返さない関数の型
label: string;
};
const MyButton = ({ onClick, label }: ButtonProps) => (
<button onClick={onClick}>{label}</button>
);
test('ボタンクリックで関数が呼ばれること', () => {
const handleClick = jest.fn(); // 監視用の偽関数
render(<MyButton onClick={handleClick} label="送信" />);
const button = screen.getByText('送信');
fireEvent.click(button); // クリックをシミュレーション
expect(handleClick).toHaveBeenCalledTimes(1); // 1回呼ばれたか確認
});
onClick: () => void; という記述が型定義です。これは「引数はなく、結果も返さない関数」という意味になります。これを決めておくことで、間違った関数を渡すミスを防げます。
6. 型の再利用と保守性の向上
テストをたくさん書いていると、同じような型を何度も書くのが大変になってきます。そんなときは、アプリ本体で使っている型をexport(エクスポート)して、テストコード側でimport(インポート)して使い回しましょう。これを「型の再利用」と言います。再利用することで、もしアプリ側の型が変わったときに、テストコードの型も自動的に同期されます。
もし別々に型を書いていると、本体を修正したのにテストコードの修正を忘れてしまい、テストが嘘をついている状態になってしまいます。初心者のうちは、できるだけ一箇所で決めたルールを使い回す習慣をつけると、後で楽になりますよ。プログラムの世界では「同じことを二度書かない」という教えがあり、これをDRY原則(Don't Repeat Yourself)と呼びます。効率よく、かつ確実に動くものを作るための大切な考え方です。
7. 非同期処理のテストにおける型
最後に、少し難しいですが大切な非同期処理について触れます。非同期処理とは、「結果が出るまで時間がかかる処理」のことです。例えば、インターネットから重い画像を持ってきている間、他の作業を止めずに待つような状態です。これをテストする場合、プログラムは「待ち」の状態になります。
TypeScriptでは、この「待ち」の状態をPromiseという型で表現します。テストコードでも、この非同期な動きを正しく扱うためにasyncやawaitといったキーワードを使います。これらを適切に使うことで、データの到着を待ってから正しく検証を行うことができます。型が「いつデータが来るか」を教えてくれるので、初心者でも迷わずにコードを書くことができます。複雑な処理こそ、型の力を借りてシンプルに整理していきましょう。