フロントエンドをユニットテストでリファクタリングする例

概要

自分が業務でよく使う、テストのない.tsxファイルなどを処理を関数で切り出し、jestでリファクタリングする手順をメモします。

リファクタ前のコード

以下は日付のフォームとバリデーション処理です。
処理はtsxに書かれていてテストはありません。

日付バリデーションの条件は当日からプラス1日〜3日です。

本記事のコードはChatGPTに書いてもらいました

import React, { useState } from 'react';

const DateForm: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState('');
  const [error, setError] = useState('');

  const validateDate = (date: string): boolean => {
    const inputDate = new Date(date);
    const today = new Date();
    const tomorrow = new Date(today.getTime() + 86400000); // 1日後
    const threeDaysLater = new Date(today.getTime() + 3 * 86400000); // 3日後

    if (inputDate >= tomorrow && inputDate <= threeDaysLater) {
      setError('');
      return true;
    } else {
      setError('Please select a date that is 1 to 3 days from today.');
      return false;
    }
  };

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const newDate = event.target.value;
    if (validateDate(newDate)) {
      setSelectedDate(newDate);
    }
  };

  return (
    <div>
      <label htmlFor="date">Select a date: </label>
      <input
        type="date"
        id="date"
        value={selectedDate}
        onChange={handleChange}
      />
      {error && <div style={{ color: 'red' }}>{error}</div>}
    </div>
  );
};

export default DateForm;

リファクタリング

バリデーション処理を切り出す

まずはテストしやすいようにバリデーションの処理を関数に切り出します。
tsxもバリデーション処理を切り出したことで見やすくなりました。

// validation.ts
export const validateDate = (date: string): boolean => {
    const inputDate = new Date(date);
    const today = new Date();
    const tomorrow = new Date(today.getTime() + 86400000); // 1日後
    const threeDaysLater = new Date(today.getTime() + 3 * 86400000); // 3日後

    return inputDate >= tomorrow && inputDate <= threeDaysLater;
};

tsxファイル

import React, { useState } from 'react';
import { validateDate } from './validation'; // バリデーション関数をインポート

const DateForm: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState('');
  const [error, setError] = useState('');

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const newDate = event.target.value;
    if (validateDate(newDate)) {
      setSelectedDate(newDate);
      setError('');
    } else {
      setError('Please select a date that is 1 to 3 days from today.');
    }
  };

  return (
    <div>
      <label htmlFor="date">Select a date: </label>
      <input
        type="date"
        id="date"
        value={selectedDate}
        onChange={handleChange}
      />
      {error && <div style={{ color: 'red' }}>{error}</div>}
    </div>
  );
};

export default DateForm;

jestを書く

日付の計算で、過去の日付などの異常値テストや、境界値の正常テストを追加しています。
これで処理の切り出しとユニットテストの導入まで行えました。

条件判定系は複雑になりがちなので、ユニットテストで担保しておくと安心ですね。

import { validateDate } from './validation';

describe('Date Validation', () => {
  const formatDate = (date: Date) => date.toISOString().split('T')[0];

  it('should accept a date exactly 1 day from today', () => {
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    expect(validateDate(formatDate(tomorrow))).toBe(true);
  });

  it('should accept a date exactly 3 days from today', () => {
    const threeDaysLater = new Date();
    threeDaysLater.setDate(threeDaysLater.getDate() + 3);
    expect(validateDate(formatDate(threeDaysLater))).toBe(true);
  });

  it('should reject a date today (boundary)', () => {
    const today = new Date();
    expect(validateDate(formatDate(today))).toBe(false);
  });

  it('should reject a date exactly 4 days from today (boundary)', () => {
    const fourDaysLater = new Date();
    fourDaysLater.setDate(fourDaysLater.getDate() + 4);
    expect(validateDate(formatDate(fourDaysLater))).toBe(false);
  });

  it('should reject a date in the past', () => {
    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() - 1);
    expect(validateDate(formatDate(yesterday))).toBe(false);
  });

  it('should reject a date far in the future', () => {
    const farFuture = new Date();
    farFuture.setDate(farFuture.getDate() + 100);
    expect(validateDate(formatDate(farFuture))).toBe(false);
  });

  it('should reject an invalid date format', () => {
    expect(validateDate('invalid-date')).toBe(false);
  });
});