背景
React(JS)の学習マイルストーンとして、以前に簡単な学習記録アプリを作成しました。
この時の記事はこちらです。
改めて説明すると、
動きとしては学習内容と学習時間を入力して登録すると
学習内容が登録されていき、累計の学習時間が表示されるというものです。
今回挑戦したこと
今回は実際のWeb開発を想定し、上記をベースに以下のような拡張を行いました。
1. Supabase上にテーブルを作成し、実際に登録できるようにした
2. FirebaseでHostingを行いアプリとして公開
3. GitHub ActionによるCICDパイプラインの作成
4. Jest react-testing-libraryを導入しUIの単体テストを導入
つまり、アプリの内容は同じでも実際のWebアプリケーション開発で行う開発プロセスにかなり近い状況を再現しているということです。
私は普段、データサイエンティストとして働いているのですが、AIアプリケーション開発もこれと近いプロセスを踏みます。一応、説明しておくとこんな感じです。
- プロデューサーがChange Request(変更要求)とJIRAチケットを作成
- データサイエンティストなどエンジニアはimplementation plan, test planを立てる
- Pythonで必要な機能と単体テストを実装
- PushするとCICDパイプラインが作動(Jenkins)
- 実装完了後、Pull Requestを作成し、他のエンジニアがレビュー、最終的にマージ
- リリース日にデータエンジニアがデプロイ
- その後のPost Validationを行う
このため、フロントエンドとバックエンドという違いはあれど、機能を実装し、テストして、リリースをしていく、その背後でCICDパイプラインが働くという構成は同じです。
学び
この取り組みを通じて、面白かったと思う点をいくつかあげます。
フロントエンド開発における単体テスト
バックエンドの場合はあるデータの入力に対して期待される出力が得られるかの単体テストやローカル環境で機械学習パイプラインを動かしてみて、問題なさそうかの検証をするのですが、フロントエンド開発ではUIなのでUIとして正しい振る舞いをしているかを検証する必要があります。
例えば、フロントエンドではUI上で"ユーザーが入力をする", "ユーザーがクリックをする"という動きがあるわけです。これに対して、コンポーネント側でバリデーションが走ったり、登録を押すとDB上へ送信され、UI上でも内容が変化するわけです。Reactのtesting-library/user-eventを使うと、単体テストでこれらのユーザーのクリックや入力を単体テストに組み込むことができます。
CICDを自分で組む
普段のAI開発業務ではPull Requestを作成すると、部署ですでに構築されているJenkinsが走るので、CICD自体がどのように実装されているのかをそこまで意識することはありませんが、今回は全て自前でやる必要があったので、どんな風にCICDパイプラインが組まれているのかをよく理解できました。
デプロイまでの一連のプロセスを体験
普段の業務では、機械学習パイプラインの実装まではデータサイエンティストの役割、そこから先の本番運用されるプロダクションへのデプロイはデータエンジニアの役割という形で役割を分けられているため、デプロイ部分は普段担当しません。これはこれで、複数人によるチェックが入ることでミスを減らせる可能性が高いので良いと思います。しかし、今回はデプロイ部分もFirebaseを使ってHostingしたので、自分で実装したサービスを自分でリリースするという体験ができました。
技術スタック
開発はReact(JS)でViteを使って開発しています。コードはGitHub上にリポジトリを作成していて、PushをするとGitHub ActionsによりCICDのパイプラインが走ります。このため、まずnpm testで単体テストが走り、これをパスするとnpm run build、さらにFirebaseを通じて、Hostingが行われます。
アプリ上から登録した内容はsupabase上に保存され、アプリ側もsupabase上のレコードをSELECTして表示するという形になっています。
ユーザーイベントを含む単体テスト
今回はいくつかのテストケースで単体テストを書いているのですが、ユーザーの入力やクリックと紐づくテストを実施しています。Reactでは@testing-libraryのuser-eventを用いることでユーザーの動きをテストに組み込むことができます。以下は、Reactにおけるユーザーのクリックなどのイベントを含めた単体テストのHello World的なコードです。
import { useState } from "react";
export const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
<button onClick={() => setCount(count - 1)}>減らす</button>
</div>
);
};
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";
describe("Counter", () => {
test("ボタンをクリックするとカウントが増える", async () => {
const user = userEvent.setup();
render(<Counter />);
expect(screen.getByText("カウント: 0")).toBeInTheDocument();
// 「増やす」ボタンをクリック
await user.click(screen.getByRole("button", { name: "増やす" }));
expect(screen.getByText("カウント: 1")).toBeInTheDocument();
});
test("ボタンをクリックするとカウントが減る", async () => {
const user = userEvent.setup();
render(<Counter />);
// 「減らす」ボタンをクリック
await user.click(screen.getByRole("button", { name: "減らす" }));
expect(screen.getByText("カウント: -1")).toBeInTheDocument();
});
});
このようにuserEventを使うことで、user.clickやuser.typeなどユーザーがUI上で行う動作をテストに組み入れることができます。
今後の展望
今回の開発を通じて、Webアプリケーション開発に必要な一連の開発を行えましたが、さらにUIの改善やTypeScriptへの移行など最終ステップに進んでいく予定です。
JISOUのメンバー募集中!
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてくださ!
▼▼▼

