Lesson 12

Next.jsとFirebaseでTODOアプリの開発 - テストの実施

Lesson 12 Chapter 1
Jest

Jestとは

アプリケーション開発においてバグやエラーを回避するためにテストは必須の工程です。 JestはJavaScriptアプリケーションのテストに使用するツールです。
Jestを使用することで、アプリケーションが正常に動作しているかどうかを確認することができます。
例えば、アプリケーションで計算を行う関数があったとします。 この関数が正しい結果を返すかどうかを確認するために、Jestを使ってテストを書くことができます。
Jestは、簡単な記述でテストを書くことができるのと、テスト結果を見やすい形式で表示することができるというメリットがあり多くの開発現場で導入されています。 Jestを活用することで、アプリケーションのテストを迅速かつ正確に行うことができます。 本レッスンではJestを使用してTODOアプリケーションの関数のテストを行なっていきましょう。

Jestの環境構築

Jestをインストールする

Jestは下記のコマンドでプロジェクトにインストールします。
npm install jest
これから実装するテストでは、必要なライブラリはjest一つのみです。 より細やかなテストを実装するには他のライブラリも必要になってきますのでご自身で調べてみてください。

Jestの設定ファイルの作成

プロジェクトルートにjest.config.jsファイルを作成し、以下を記述していきます。

jest.config.js
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  //プロジェクトルートを相対パスで指定
  dir: './',
})

// jestの設定を以下記載
const customJestConfig = {
  moduleDirectories: ['node_modules', '/'],
}

// export
module.exports = createJestConfig(customJestConfig)

上記のファイルは、Jestを使ってNext.jsプロジェクトをテストするための設定ファイルです。 最初に、next/jestモジュールをインポートし、nextJest変数に代入します。next/jestモジュールはNext.jsでJestの設定を作成するために必要な関数を提供します。
次にcreateJestConfigという変数に、nextJest関数を呼び出して作成された設定を代入します。この関数ではプロジェクトのルートディレクトリを相対パスで指定するためのdirオプションを設定します。
その次にcustomJestConfigという変数に、Jestの設定を定義します。
この設定ではmoduleDirectoriesプロパティを指定します。このプロパティはモジュールの検索を行うディレクトリを指定するものです。ここではnode_modulesとルートディレクトリ(/)を指定します。 最後に、module.exportsでcreateJestConfig関数にcustomJestConfigを渡して最終的なJestの設定をエクスポートします。

テスト実行用のnpmスクリプトを追加

テスト実行のnpmスクリプトを追加します。

package.json
"scripts": {
  ...
  "test": "jest --watch",
  ...
}

上記のスクリプトの追加で「npm test」の実行時にjestを実行するとnode.jsが解釈してくれます。 テストを実行する際はターミナルで上記の「npm test」を実行することでテストができます。

Jestの関数テストの基本的な書き方

本レッスンではJestのtoHaveBeenCalledWith関数を使用してTODOアプリの関数をテストします。 toHaveBeenCalledWith関数は、関数が呼び出されたときに特定の引数が渡されたかどうかをテストするために使用されます。
以下のtoHaveBeenCalledWithを使用したテストの例を見てみましょう。まず、以下のような関数があると仮定します。

function add(a, b) {
  return a + b;
}

この関数は引数aとbを受け取り、それらを加算して返却する関数です。 add関数が引数2と3で呼び出された場合をテストするには、以下のようにします。

describe("funcTest", () => {
    test('add関数が2と3で呼び出されたときに5を返すことを確認する', () => {
        const mockFn = jest.fn(add);
        const result = mockFn(2, 3);
        expect(mockFn).toHaveBeenCalledWith(2, 3);
        expect(result).toBe(5);
    })
});

テストファイルの記述の説明をします。 describeという関数で似た様なテストコードをブロックとして管理します。第一引数はブロック名で第二引数に実際に実行するテストを記載していきます。 基本はこのdescribeの第二引数にテストコードを記述していきます。 テストコードはtest関数の記述から始めて第一引数にテスト名、第二引数にテストコードを記述していきます。 このテストでは、jest.fnという関数のモックを作成する関数を使用してadd関数のモックを作成し、それを引数2と3で呼び出します。 toHaveBeenCalledWithアサーションを使用して、mockFnが引数2と3で呼び出されたことを確認し、 toBeアサーションを使用して、add関数が正しい結果を返すことを確認します。

次のチャプターでは実際にTODOアプリをJestでテストしていきましょう。

Lesson 12 Chapter 2
テストコードの実装

事前準備

それではTODOアプリの「addTodo」「editTodo」「deleteTodo」をテストする為各関数をexportする必要があります。 ページコンポーネント内では各関数をexportできませんのでcomponentsフォルダ内にtodoFunc.jsxというファイルを新規作成し3つの関数を記述していきます。

todoFunc.js
import { addDoc, collection, deleteDoc, doc, setDoc } from "firebase/firestore";
import { db } from "../firebase";

export const addTodo = async (input) => {
    await addDoc(collection(db, "todos"), { todo: input });
};

export const editTodo = async (id, updateTodo) => {
    await setDoc(doc(db, "todos", id), { todo: updateTodo });
};

export const deleteTodo = async (id) => {
    await deleteDoc(doc(db, "todos", id));
};

テストとは関係ないですが、index.jsxも書き直しておきましょう。

index.jsx
import { addTodo, deleteTodo, editTodo } from "../components/todoFunc";  //import部分
〜〜省略〜〜
const AddTodo = (input) => {
  addTodo(input);
};

const EditTodo = (id, input) => {
  editTodo(id, input);
};

const DeleteTodo = (id) => {
  deleteTodo(id);
};
〜〜省略〜〜
// 各onClickイベントの記載を変更
<button onClick={() => AddTodo(input)}>登録</button>
<button onClick={() => EditTodo(todo.id, updateTodo)}>更新</button>
<button onClick={() => DeleteTodo(todo.id)}>削除</button>

テストコードの記述

これでテスト対象の3つの関数をexportすることができました。それでは実際にテストコードの記述をしていきましょう。 プロジェクトルートに__tests__フォルダを作成してその中にindex.test.jsファイルを作成します。

index.test.js
import { addDoc, collection, deleteDoc, doc, setDoc } from "firebase/firestore";
import { addTodo, deleteTodo, editTodo } from "../components/todoFunc";
import { db } from "../firebase";

jest.mock("firebase/firestore", () => ({
    addDoc: jest.fn(),
    collection: jest.fn(),
    deleteDoc: jest.fn(),
    doc: jest.fn(),
    setDoc: jest.fn(),
}));

jest.mock("../firebase", () => ({
    db: {},
}));

describe("addTodo", () => {
    test("addTodo test", async () => {
        const input = "new todo";
        await addTodo(input);
        expect(addDoc).toHaveBeenCalledWith(collection(db, "todos"), {
            todo: input,
        });
    });
});

describe("editTodo", () => {
    test("editTodo test", async () => {
        const id = "todo-123";
        const updateTodo = "updated todo";
        await editTodo(id, updateTodo);
        expect(setDoc).toHaveBeenCalledWith(doc(db, "todos", id), {
            todo: updateTodo,
        });
    });
});

describe("deleteTodo", () => {
    test("deleteTodo test", async () => {
        const id = "todo-123";
        await deleteTodo(id);
        expect(deleteDoc).toHaveBeenCalledWith(doc(db, "todos", id));
    });
});

addTodoのテスト記述を例に挙げて説明します。まず最上部のjest.mock関数でfirebaseのaddDoc関数をモック化します。 その後addTodoにテスト文字列を引数で渡して実行します。 最後にexpect関数でモック化したaddDocにaddTodo関数実行時渡した引数が正常に渡せて関数を呼び出せているかをテストしています。

テストの実行

それではnpm testコマンドを実行してみましょう。
下記の様にテストがPASSとなればテスト成功です。

おわりに

本チャプターではJestによるテストの実行方法について学んでいきました。 Jestには今回実施した関数の単体テストのみではなくデザインのテストを行うスナップショットテストや結合テスト、いろいろなテストを実施できます。 興味がある方は是非調べてみてください。

nextjs-12-1