はじめに
jisouの課題で学習記録アプリを作成しており、今回Github ActionsのCI/CDでテストがエラーとなったため、原因と解決策について記事にします。
エラー内容
Github Actionsでpush時にtest → build → deployと設定しているのですが、上記画像のようにbuild時にエラーが発生しました。
エラー箇所について詳しく見てみると、buildの前のtest時にエラーが発生しているようで、exit code 1 → 異常終了(エラーや失敗)で処理が終了していることが分かります。
ローカル環境ではtestがうまくいっており、なぜCI/CD時にはエラーになるのかが分かりませんでした。
解決策
何度かコードを修正pushしている時に偶然一度だけCI/CDでのテストが通り、その後testに関係のないREADMEを修正し再度pushしましたが、またtestが通らなくなり、testが通った時のコードに戻してから強制的にpushをしてみてもtestが通らなくなりました。
ここで、testを実行するコード自体の内容には問題はなさそうだが(ローカルではほぼ成功していたため)testの安定性に問題があるのではと思い、調べてみました。
ここで全く同じようなエラーに遭遇しているQiitaの記事を発見し参考にしましたところ、ローカルテスト、CI/CDテスト両方とも安定して通るようになりました。
↓参考にした記事
https://qiita.com/ritsu21ctws/items/7f66315ab3ade8565c10#comments
テストの実行時間と結果を表にまとめられていて、非常に分かりやすい記事でした。
この記事を参考にテスト結果を取得するexpectにタイムアウトを設定したところ、安定してテストを通せるようになりました。
※コードのtimeoutが今回の修正した部分です。
修正したコード
import React from "react";
import { Todo } from "../Todo";
import '@testing-library/jest-dom';
import { render, screen, waitFor, fireEvent, waitForElementToBeRemoved } from "@testing-library/react";
describe("Title Test", () => {
it("タイトルが学習記録一覧テスト版であること", async () => {
render(<Todo initialData={[]} />);
const title = await screen.findByTestId("title");
await waitFor(async() =>{
expect(title).toHaveTextContent("学習記録一覧テスト版");
}, { timeout: 2000 })
});
});
describe("Todo 登録テスト", () => {
it("学習内容と時間を入力して登録すると1件追加される", async () => {
render(<Todo initialData={[
{ id: 1, content: "既存の学習", time: 1 },
]} />);
// 初期のリスト数を取得
await waitForElementToBeRemoved(() => screen.getByText("Loading..."));
const initialRecords = (await screen.findAllByTestId("record-item")).length;
screen.debug();
// フォーム入力 & 登録ボタンをクリック
const contentInput = await screen.findByPlaceholderText("テキストを入力");
const timeInput = await screen.findByPlaceholderText("0");
const addButton = await screen.findByRole("button", { name: "登録" });
// 🔁 fireEventで値を変更
fireEvent.change(contentInput, { target: { value: "React Testing" } });
fireEvent.change(timeInput, { target: { value: "2" } });
// 🔁 fireEventでボタンクリック
fireEvent.click(addButton);
// 追加後のリスト数をチェック
await waitFor(async () => {
const newRecords = (await screen.findAllByTestId("record-item")).length;
expect(newRecords).toBe(initialRecords + 1);
}, { timeout: 2000 }); // タイムアウト設定を追加);
});
});
describe("Todo 削除テスト", () => {
it("削除ボタンを押すと1件削除される", async () => {
render(<Todo initialData={[
{ id: 1, content: "React Testing", time: 2 },
{ id: 2, content: "Unit Test", time: 3 },
]} />);
// 初期のリスト数を取得
await waitForElementToBeRemoved(() => screen.getByText("Loading..."));
const initialRecords = (await screen.findAllByTestId("record-item")).length;
// 削除ボタンをクリック
const deleteButton = screen.getAllByRole('button', { name: '削除' })[0];
fireEvent.click(deleteButton);
// 削除後のリスト数をチェック
await waitFor(async () => {
const newRecords = (await screen.findAllByTestId("record-item")).length;
expect(newRecords).toBe(initialRecords - 1);
}, { timeout: 2000 }); // タイムアウト設定を追加);
});
});
describe("Todo 未入力フィールドテスト", () => {
it("入力せずに登録するとエラーメッセージが表示され、リスト数は変わらない", async () => {
render(<Todo initialData={[]} />);
// 「Loading...」が消えるのを待つ(データ取得系処理がある場合)
await waitForElementToBeRemoved(() => screen.getByText("Loading..."));
// 初期のリスト数を取得
const initialRecords = (await screen.findAllByTestId("record-item")).length;
// 登録ボタンを取得してクリック(入力は空のまま)
const addButton = await screen.findByRole("button", { name: "登録" });
fireEvent.click(addButton);
// エラーメッセージが表示されていることを確認
expect(await screen.findByText("入力されていない項目があります")).toBeInTheDocument();
// リスト数が変わっていないことを確認
const currentRecords = (await screen.findAllByTestId("record-item")).length;
expect(currentRecords).toBe(initialRecords);
});
});
おわりに
今回のエラーが課題2の中で一番躓いた点で、およそ2日間はエラーにハマってしまいました。
ただ、どういった点がエラーになっているのか、そもそもコードの処理の内容がおかしいのか、といった観点だけでなく、CI/CDといった外部のサービス上で実行する際に何が問題になるのか、といった観点が必要になることが大きな学びでした。
正直、今回参考にした記事へすぐに辿り着いていれば解決も早かったと思いますが、なぜエラーが発生しているのか、なぜこの解決策が有用なのか、といった解決までのステップを自分で考えることが、開発には必要な力なのかと考えるようになりました。
まだまだ躓いた点はあるので、記事に残していきたいと思います。
JISOUのメンバー募集中!
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてくださ!
▼▼▼