5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactアプリケーションのテスト:学習記録アプリの削除機能をテストする

Last updated at Posted at 2024-07-24

はじめに

Reactで作成した学習記録アプリケーションのテストを書く過程で、特に削除機能のテストに苦戦しました。その過程と解決方法、React Testing Libraryを使用したテストの書き方について記載します。

テスト対象のコード

App.jsx
function App() {
  const [records, setRecords] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchRecords();
  }, []);

  const fetchRecords = async () => {
    setLoading(true);
    const { data, error } = await supabase
      .from('study-record')
      .select('*');

    if (error) {
      console.error("Error fetching data: ", error);
    } else {
      if (Array.isArray(data)) {
        setRecords(data);
      }
    }
    setLoading(false);
  };

  const handleDelete = async (id) => {
    const { error } = await supabase
      .from('study-record')
      .delete()
      .eq('id', id);

    if (error) {
      console.error("Error deleting data: ", error);
    } else {
      setRecords(records.filter(record => record.id !== id));
    }
  };

  // ... render部分 ...
}

このコードでは:

  1. fetchRecords関数でSupabaseから学習記録を取得し、records状態を更新しています。
  2. handleDelete関数で特定のIDの記録を削除し、成功した場合はrecords状態から該当の記録を除外しています。
  3. コンポーネントのマウント時にfetchRecordsを呼び出して初期データを読み込んでいます。

これらの機能、特に削除機能が正しく動作することをテストしていきます。

問題

初めに作成したテストコードは以下:

test('学習記録を削除できること', async () => {
  const mockData = [
    { id: 1, title: '既存の学習内容1', time: 1 },
    { id: 2, title: '既存の学習内容2', time: 2 },
  ];

  jest.spyOn(supabase, 'from').mockImplementation(() => ({
    select: jest.fn().mockResolvedValue({ data: mockData, error: null }),
    insert: jest.fn().mockResolvedValue({ data: [{ id: 3, title: '新しい学習内容', time: 3 }], error: null }),
    delete: jest.fn().mockResolvedValue({ error: null }),
  }));

  render(<ChakraProvider><App /></ChakraProvider>);

  await screen.findByText(/Github actionsでデプロイした学習記録アプリ/i);

  expect(screen.getAllByText(/既存の学習内容/)).toHaveLength(2);

  const deleteButtons = screen.getAllByLabelText('delete');
  fireEvent.click(deleteButtons[0]);

  await waitFor(() => {
    expect(screen.getAllByText(/既存の学習内容/)).toHaveLength(1);
  });

  expect(screen.queryByText('既存の学習内容1')).not.toBeInTheDocument();
  expect(screen.getByText('既存の学習内容2')).toBeInTheDocument();
});

しかし、このテストを実行すると以下のようなエラーが発生しました:

  1. supabase is not defined
  2. TypeError: Cannot destructure property 'data' of '(intermediate value)' as it is undefined.
  3. _supabaseClient.default.from(...).delete(...).eq is not a function

原因

これらのエラーの主な原因は以下でした:

  1. supabaseクライアントのモックが適切に設定されていなかった
  2. 非同期処理の扱いが適切ではなかった
  3. テスト環境でのレンダリングと状態更新の扱いが不適切だった

解決策

問題を解決するために、以下の修正を実施しています

1.supabaseクライアントのモックを適切に設定:

jest.mock('../supabaseClient', () => ({
  from: jest.fn().mockReturnValue({
    select: jest.fn().mockResolvedValue({ data: [], error: null }),
    insert: jest.fn().mockResolvedValue({ data: [], error: null }),
    delete: jest.fn().mockReturnValue({
      eq: jest.fn().mockResolvedValue({ error: null })
    })
  })
}));

2.テストケースの修正:

test('学習記録を削除できること', async () => {
  const mockData = [
    { id: 1, title: '既存の学習内容1', time: 1 },
    { id: 2, title: '既存の学習内容2', time: 2 },
  ];

  supabase.from.mockReturnValue({
    select: jest.fn().mockResolvedValue({ data: mockData, error: null }),
    delete: jest.fn().mockReturnValue({
      eq: jest.fn().mockResolvedValue({ error: null })
    })
  });

  await act(async () => {
    render(
      <ChakraProvider>
        <App />
      </ChakraProvider>
    );
  });

  await screen.findByText(/Github actionsでデプロイした学習記録アプリ/i);

  expect(screen.getAllByText(/既存の学習内容/)).toHaveLength(2);

  const deleteButtons = screen.getAllByLabelText('delete');
  await act(async () => {
    fireEvent.click(deleteButtons[0]);
  });

  await waitFor(() => {
    expect(screen.getAllByText(/既存の学習内容/)).toHaveLength(1);
  });

  expect(screen.queryByText('既存の学習内容1')).not.toBeInTheDocument();
  expect(screen.getByText('既存の学習内容2')).toBeInTheDocument();
});

3.App.jsxfetchRecords 関数の修正:

const fetchRecords = async () => {
  setLoading(true);
  try {
    const { data, error } = await supabase
      .from('study-record')
      .select('*');

    if (error) throw error;

    if (Array.isArray(data)) {
      setRecords(data);
    }
  } catch (error) {
    console.error("Error fetching data: ", error);
  } finally {
    setLoading(false);
  }
};

これらの修正により、テストが正常に通過するようになりました。特に、削除後にレコードの数が1つ減っていることを確認できるようになりました。

おわりに

テストそのものの経験がほとんどなかったので苦戦しました。中でも外部APIを使用する部分のテストは難しい場合がある様で、適切なモッキングと非同期処理の扱いという概念を学ぶまでに時間がかかりました。

主に以下のことを学びました:

  1. 外部依存関係(今回ははsupabase)の適切なモッキングの重要性
  2. React Testing Libraryでの非同期処理の扱い方
  3. act を使用した状態更新の適切な処理

削除の他にも、
・タイトルの表示
・レコードの追加
・入力エラー
についてもテストすることができました。
このテストに不随して、CIも終わらせることができたので、CI/CD、テストと一連を学ぶことができプログラミングスキルが少し上がった気がします。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?