テストをしてみよう（単体テスト）

この記事は、「[架空プロジェクトを通してシステム開発とドキュメント作成を体験してみる（2022 Late）](https://qiita.com/minimumskills/items/aedf6a93658db3f74747)」の記事の一部です。

# 概要
ここでは、APIに対する単体テストを実行してみます。
単体テストとはAPI等の最小機能単位に対して正常に動くかテストを行うことです。

単体テストのカテゴリには

* 正常系テスト（仕様通りに正しく動くか）
* 異常系テスト（仕様通りに正しくエラーとなるか）
* 境界値系テスト（○○以上、○○未満等の処理が正しく動作するか）

などがあります。

>ここでは正常系、異常系をサンプル程度に行ってみます。

テストは手作業で実施することもできますが、改修が入るごとにテストを手作業で実施するのはとても大変です。
テストを自動化することで、変更があるたびに必要なテストを自動で行えるようになり効率的に開発が行えるようになります。

ここではJavaScriptのテストツールとしてメジャーな「Jest」と、Jest上で実行できるAPIテストツールの「frisby」を利用してみます。

> [Jestの公式サイト](https://jestjs.io/ja/)
>Jestはシンプルさを重視したテスティングフレームワークで、細かい設定不要で実行できることを目指して作られています。また、テストが失敗したときのエラーメッセージもわかりやすく表示してくれます。

> [frisby.jsの公式サイト](https://docs.frisbyjs.com/)
> frisbyはRest APIを簡単にテストすることを目的としたJavaScriptのフレームワークです。

# 環境構築

## node.jsインストール

jestとfrisbyはNode.jsというJavaScriptをサーバやPC等のブラウザ以外の環境で動かすツールに依存しているので、Node.jsをインストールします。

https://nodejs.org/ja/

[Node.jsの公式サイト](https://nodejs.org/ja/)からダウンロードしてきます。
LTS版をクリックしてダウンロードします。

>LTSとはLong Term Supportの略で、長期間サポート版という感じなので、特に理由が無い限りLTS版を利用するのが無難です。

![000001.jpg](https://drive.google.com/uc?export=view&id=1NtN-7YW3cvd8LsMqaWUdEA_0H9DjMM4v)

> Windows版のインストールは[こちら](https://qiita.com/minimumskills/items/b34fbd5352badcd6171d)を参考にしてください。

>開発者はインストーラー版ではなく、複数のNode.jsを切り替えて使えるツール(nodenv）などを介してインストールすることが多いです。

ダウンロードが完了したらダウンロードフォルダを確認し、nodeパッケージファイルをダブルクリックします。


![000010.jpg](https://drive.google.com/uc?export=view&id=1rge_GaY15DeZLzSr750evw2QT_J77wjg)

インストーラが起動するのでガイダンスに沿ってインストールします。

![000020.jpg](https://drive.google.com/uc?export=view&id=1MNdO4JvY9FJRScsGmN9GFqPeT_FkEoEc)

![000030.jpg](https://drive.google.com/uc?export=view&id=1eyk28ZK8Nh91_Kx8z4wjKdVybbIwy53_)

![000040.jpg](https://drive.google.com/uc?export=view&id=1wIU29fPaPphoyc0qMo06SHnZoRO4GUEt)

![000050.jpg](https://drive.google.com/uc?export=view&id=1zlTEktkhuSR9S7SUcQFp7ZpPtKLL5xEO)

![000060.jpg](https://drive.google.com/uc?export=view&id=1yrGxuBa7fADp17u5NrKIgH4M3ifS91_0)

![000070.jpg](https://drive.google.com/uc?export=view&id=1y5KrRH5kmcJtaqz_KvVDL4ZdRQmBAj4L)

![000080.jpg](https://drive.google.com/uc?export=view&id=1EBDz_GO0Ssk5Xhp11REFOF3_mrNPMXUt)

完了したら、インストーラは不要なのでゴミ箱に入れてしまっていいです。

![000090.jpg](https://drive.google.com/uc?export=view&id=1FbobsyvkTumXX1oTg31NmX1JRp9DyY0v)

## 確認

ターミナル（Windowsの場合はコマンドプロンプト）を開いてバージョンを確認してみます。

```
node --version
```
インストールしたバージョンと同じバージョン情報が返ってきていればインストール成功です。
次に以下のコマンドも実行してみてください。

```
npm --version
```

npmはNode.jsで利用する様々な追加機能をインストールするために使うパッケージマネージャというツールです。

>実際にはnodeコマンドよりnpmコマンドを利用することの方が多いです。

## パッケージのインストール

まず、package.jsonというファイルを生成します。
package.jsonにはnpmでインストールされたパッケージの設定情報が記述されるファイルですが、存在していないとエラーがでる場合があるので生成しておきます。

ターミナルを開いてローカル環境のwebsiteフォルダの中に移動します。

> 現在の位置を確認するコマンドは`pwd`と入力してエンターを押すと確認できます。

```bash
cd ~/Desktop/website  #デスクトップのwebsiteフォルダに移動
```
移動できたらwebsiteフォルダ内にpackage.jsonを生成します。

```bash
npm init -y #-yオプションをつけるとすべてデフォルトの値の状態でpackage.jsonが生成される
```
ターミナルが返ってきたらフォルダ内をVSCodeで確認してみます。

> ターミナルはまだ使うので閉じずにそのままにしておきます。

package.jsonが追加されているのが確認できます。

![000100.jpg](https://drive.google.com/uc?export=view&id=1tGT-zAoAiNpS5G6LZPSPkiPu2LGHCHE5)

確認ができたのでJestとfrisbyをインストールします。
ターミナルで下記を実行します。

> `i`はinstallのiです。

```bash
npm i jest
npm i frisby
```

>この操作はかならずwebsiteディレクトリ内で実施してください。じゃないと思わぬところに変なゴミファイルができてしまいます。
>いくつかWARNが出るかもしれませんが、基本的に問題ありません（エラーはダメです）。

しばらくするとインストールが完了してターミナルが返ってきます。
VSCodeで確認すると、package.jsonのdependenciesにfrisbyとjestが追加され、さらにpackage-lock.jsonというファイルとnode_modulesというフォルダが追加されています。

> package-lock.json はパッケージインストール時に自動生成されます。パッケージのバージョンがインストールのタイミングによって変わってしまわないよう（基本最新バージョンがインストールされてしまうため）にバージョンを固定して管理してくれるものです。
node_modules はインストールしたパッケージが実際に入っているフォルダです。npmでインストールしたパッケージは全てこの中にはいります。

![000110.jpg](https://drive.google.com/uc?export=view&id=1k1JXaZ0GG-8Ots1WMAC79lcisRJSCBK6)

# テストを書く

## テストフォルダ・ファイルの用意

Jestはプロジェクトフォルダの直下に`__tests__` フォルダ作を作り、その中にテスト用のスクリプトファイルを入れて管理するか、`xxx.test.js`という拡張子をつけたファイル名にするとテストを実行してくれます。

今回のテストは__tests__フォルダで管理していきますので、websiteフォルダ内に__tests__というフォルダを作成します。
__tests__フォルダ内にAPIテストプログラムを記述したファイルを作っていきます。「api.js」というファイルを作成します。
作成すると、下記のようなファイル構造になっています。

![000120.jpg](https://drive.google.com/uc?export=view&id=1uS9OiIrhgbXQQJG8Ep2G5Smwhh9ArSlz)

## テストプログラム実装
このテストでは4つのテストを実行します。

- 正常
    - お名前・Email・問合わせ内容が正しく設定されて送信された場合、jsonでmessageが"success!"と返ってくる。
- 異常：お名前が未設定
    - 必須項目のお名前が未設定（空）の場合、jsonでmessageが"validation error!"と返ってくる（jsonが想定どおり返ってくることを正とする）
- 異常：Emailの形式が不正
    - Emailの形式が正規表現で指定したルールと一致しない場合、jsonでmessageが"validation error!"と返ってくる（jsonが想定どおり返ってくることを正とする）
- 異常：問合せ内容が不正
    - 問合わせ内容が10文字以上の場合、、jsonでmessageが"validation error!"と返ってくる（jsonが想定どおり返ってくることを正とする）

> 実際は上記のパターンの他に、Emailが未設定の場合、問合わせ内容が未設定の場合、全てが未設定の場合、お名前とEmailが未設定の場合・・・、正常な文字数でエラーになるか？など考えられる全てのパターンをテストする必要があります（通常は数十パターンになることが一般的です）。
   
### frisbyの読み込みとAPIのURL設定
まず、ファイルの上部でfrisbyを読み込み、api_urlを指定します。

```__tests__/api.js
const frisby = require("frisby"); //frisbyを読み込む

//APIのURLを指定
const api_url = "https://script.google.com/macros/s/{デプロイID}/exec";
```

### 正常系

各項目が正常な値の場合のテストを記述します。
frisby.postでAPIに指定したパラメータを投げます。
expectで返ってきた値と期待する値を比較します（例`expect(返ってきた値).toEqual(期待値);`）。

テキストで返ってきた値（`res._body`）をJSON.parseしてJSONに戻し、期待値（`{ "message": "success!" }`）のJSONと比較します。
JSONの値を比較する場合は「toEqual」または「toStrictEqual」を使います。

```api.test.js
//正常系
it("正常系", async () => {

    const res = await frisby.post(api_url, {
        body: "name=aaa&email=test@test.local&body=foooooo",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    });
  
    expect(JSON.parse(res._body)).toEqual({ "message": "success!" });
});
```
保存してテストを実行してみます。

ターミナルを開いてwebsiteフォルダに移動します。

```bash
cd ~/Desktop/website
```
テストを実行するコマンドは以下の通りです。

```bash
npx jest
```

> npxはnode package executerの略でパッケージを実行するツールです。

テストが走り出します。

![000130.jpg](https://drive.google.com/uc?export=view&id=1Yaw-BPU6n_zLJ1B0AS3MLS7HXuv2_HaU)


少しするとテスト結果が返ってきます。
[PASS]と緑色で表示されその横にPASSしたテストのファイル名が表示されています。
その下にはチェックマークが入り、正常系のテストがクリアしたことがわかります。

![000140.jpg](https://drive.google.com/uc?export=view&id=1q0m24xh6SKZkL6xsZLGJz9ClvS6oIzeb)




### 異常系：name不正
同様の手順でお名前が未設定の状態のテストを作成します。
正常系をコピーして、bodyの`name=aaa`を`name=`に変更、比較するmessageの値を`validation error!`にします。（API作成時にAPI側でバリデーションチェックで引っかかった場合は「validation error!」と返すように設定してました。）

```__tests__/api.js
//name不正
it("異常系：name不正", async () => {

    const res = await frisby.post(api_url, {
        body: "name=&email=test@test.local&body=foooooo",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    });

    expect(JSON.parse(res._body)).toEqual({ "message": "validation error!" });
});
```
保存してターミナルから再度テストを実行します。

```bash
npx jest
```
テスト結果に異常系：name不正を追加され、こちらもPASSしたことがわかります。
![000150.jpg](https://drive.google.com/uc?export=view&id=1du7rBC8gOLckBoJvyw1ovKhhuVJH8Fyy)
試しに、このテストを正常系の値（`name=`を`name=aaa`と記述して保存）にしてテストを実行してみます。

```
body: "name=aaa&email=test@test.local&body=foooooo",
```
すると、テストはFAILになりました。
正常系にはチェックが入っていますが、異常系：name不正は赤で☓が記されています。
期待値は"message": "validation error!"ですが受け取った値は「"message": "success!"」だということがわかります。

![000160.jpg](https://drive.google.com/uc?export=view&id=1rotG7qtFJgx3yalsGD-Ycj68kMVppPgt)


テストに問題があった場合の挙動がわかったところでname不正のテストの値は`name=`に戻して保存しておきます。

### 異常系：Email不正
同様の手順でEmailのテストを記述します。emailの値は「test」にしました。

```__tests__/api.js
//Email不正
it("異常系：Email不正", async () => {
    const res = await frisby.post(api_url, {
        body: "name=aaa&email=test&body=foooooo",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    });

    expect(JSON.parse(res._body)).toEqual({ "message": "validation error!" });
});
```

### 異常系：問合せ内容不正
同様の手順で問合わせ内容のテストを記述します。
今回は問合わせ内容の値は10文字以下が正常なので、10文字以上にします。

```__tests__/api.js
//問い合わせ不正
it("異常系：問合せ内容不正", async () => {
    const res = await frisby.post(api_url, {
        body: "name=aaa&email=test@test.local&body=foooooooooooooo",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    });

    expect(JSON.parse(res._body)).toEqual({ "message": "validation error!" });
});
```

保存したら、テストを実行してみます。
全てクリアすると以下のような結果が返ってきます。
全てのテストを実行して全てグリーンチェックが入ることが理想です。
![000170.jpg](https://drive.google.com/uc?export=view&id=15tAE3lMqJPdejbhKON2Jl-9nnRxmXwxy)


# 最終的なコード

```__tests__/api.js
const frisby = require("frisby"); //frisbyを読み込む

//APIのURLを指定
const api_url = "https://script.google.com/macros/s/{デプロイID}/exec";
//正常系
it("正常系", async () => {

    const res = await frisby.post(api_url, {
        body: "name=aaa&email=test@test.local&body=foooooo",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    });

    expect(JSON.parse(res._body)).toEqual({ "message": "success!" });
});

//name不正
it("異常系：name不正", async () => {

    const res = await frisby.post(api_url, {
        body: "name=&email=test@test.local&body=foooooo",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    });

    expect(JSON.parse(res._body)).toEqual({ "message": "validation error!" });
});

//Email不正
it("異常系：Email不正", async () => {
    const res = await frisby.post(api_url, {
        body: "name=aaa&email=test&body=foooooo",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    });

    expect(JSON.parse(res._body)).toEqual({ "message": "validation error!" });
});

//問い合わせ不正
it("異常系：問合せ内容不正", async () => {
    const res = await frisby.post(api_url, {
        body: "name=aaa&email=test@test.local&body=foooooooooooooo",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    });

    expect(JSON.parse(res._body)).toEqual({ "message": "validation error!" });
});
```

# まとめ

* 単体テストとはAPI等の機能単位をテストする工程
  * 一般的には開発工程で開発とセットで行う（一定の工数がかかる）
  * 先にテストを記述してから機能を実装するテスト駆動開発（TDD）という品質向上手法もある
* 手動でもできるが、自動化することが一般的
  * コードを改変したときに（自動）実行させて、バグが発生してないかチェック

# ドキュメント作成視点での考察

* 単体テストの実施の有無、内容はどこにどう記述すべき？
* テスト結果はどのように評価すればよいか？（それはどこにどう記述すべき？）
* テストの内容が妥当（十分）であることをどう証明するか？

# 関連コンテンツ

* 次：[テストをしてみよう（結合テスト・シナリオテスト）](https://qiita.com/minimumskills/items/7153b49cff01dd22d37a)
* 前：[動作確認（サービスの公開）](https://qiita.com/minimumskills/items/e9661e5ea233d060ce83)