テストってどうやればいいの？

## この記事について

筆者が実務経験から得た、テスト全般についてのノウハウを記載します。
難しい技術を前提とした説明はありません。主にテストをするうえでの考え方に焦点を当てています。

筆者の実務経験の内容は、Webシステムやスマホアプリ、社内向けシステムやECサイトの開発が多いです。
そのため、記載内容はこれらに偏っているかもしれませんが、根本的なところはテスト全般に当てはまることが多いと思います。

テストについてのすべてを書くことは到底できませんが、何かの役に立つことができれば幸いです。


## テストとは何か？

テストとは、例えて言えば「バグという獲物を捕まえる狩り」です。
テストケースを作ることは、罠を仕掛けることに似ています。
アドホックテストは、獲物を追い立てて弓や銃で仕留めるタイプの狩りと言えるでしょう。

![trap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27823/62167904-f0cd-9d27-580e-f0399e538d5e.png)

狩りをするとき第一に考えなければならないことは、捕まえたい獲物を特定することでしょう。その上でその獲物の習性を知り、それが通る確率の高い場所に罠を仕掛けたり待ち伏せたりしなければ捕まえることは難しいでしょう。

捕まえたい獲物が兎なのか鹿なのかも分からず、闇雲に罠を仕掛けても効果はありません。
しかし、どんなバグを見つけたいのかをあまり意識せずにテストケースを作る人は案外多いのではないでしょうか。
まず発生しうるバグのタイプを想定し、それを見つけるためにはどんなテストを行なえばいいかを考える、これが望ましいアプローチです。

テストの目的は、バグを見つけることです。バグが見つかる見込みの薄いテストを行ってもあまり意味はありません。
テストを実施することによってテスト対象の理解が深まることはありますが、それはあくまで副次的なものです。それで満足してはいけません。
ただし、見つかる見込みが薄くとも、発生した場合の深刻度が高いバグについては重点的にテストを行ったほうがよいでしょう。


## すべてのステップを実行してみる

記述したプログラムコードは、最低でも1回は実行しましょう。
やって当然のことなのですが、できていないことが結構あるのではないかと思います。1回でも実行していれば動かないと分かったであろうバグが、システムテストや本番運用が始まってから見つかったということを何度も目にしてきました。

正式なドキュメントを参照して書いたコードであっても、環境要因やバージョンの違いなどによって期待通りには動かないことはあります。ドキュメントが間違っていることもあります。実際に動かしてみなければ保証はできません。
そのため、開発/テスト環境のプラットフォームのバージョンなどは本番環境とできる限り同じにすることが望ましいです。

エラーハンドリングの処理など、特殊な条件の場合にのみ実行されるコードをテストをするには手間がかかることがあります。
そのような場合は、最初にエラーハンドリング処理だけを実装して、それが動くことを確認してから正常系の処理を実装すると効率がよいです。

```js
// ボタンクリック時の処理 (テスト対象)
function onClickXXX() {
    // まずはこれだけを実装して、handleErrorが期待通り動くことを確認する
    handleError(new Error("test"));
}

/////////////////////////////////////////////////////////////////////////

function onClickXXX() {
    // 本来の処理を実装する
    try {
        doSomething();
    } catch(error) {
        handleError(error);
    }
}
```

コードが変わったことにより、動作が変わったりデグレが発生したりする可能性はあります。
しかし、何もテストしないよりは、こんなやり方でもやったほうがずっとよいです。それで見つかるバグはあるからです。

ユニットテストフレームワークやモックツールなどを利用してきちんとした単体テストを行なうかどうかは、品質と工数のトレードオフになります。
カバレッジの測定についても同様です。カバレッジレポート上では実行されていないコードでも、何らかの方法で実行して確認できているならば、網羅にこだわる必要はないでしょう。

また、テスト対象のコードを書き換えず、かつモックツールなどを使わずとも少し工夫すればエラーを発生させられる場合があります。
以下に例として、Web API周りの機能のエラーを発生させる方法を挙げます。（Web APIはすでに動くものがあるとします）

| エラー                       | 発生方法                                           |
| ---------------------------- | -------------------------------------------------- |
| ネットワークが不通           | OSのネットワーク接続を無効にする                   |
| URLが不正                    | コードや設定ファイルのURLを変更する                |
| タイムアウト                 | タイムアウトのしきい値を小さくする                 |
| 認証エラー                   | アクセストークンに不正な値を渡す                   |
| レスポンスデータの構造が不正 | レスポンスデータがマッピングされる項目名を変更する |


## データの多重度に着目する

RDB（リレーショナルデータベース）には、エンティティの関連における多重度（カーディナリティ）という概念があります。
RDBに限らず、データ同士の関連を考えるとき、この概念は重要です。

多重度は以下のパターンに分けられます。

* 厳密に1つ
    * 例: ユーザ情報において、「氏名」は必須で、複数は不可
* 0または1
    * 例: ユーザ情報において、「生年月日」は必須ではないが、複数は不可
* 1ないし複数
    * 例: ユーザ情報において、「メールアドレス」は必須だが、複数の場合もある
* 0ないし複数
    * 例: ユーザ情報において、「所属組織」は0の場合もあるし複数の場合もある (所属先の決まっていない新入社員などは0、兼務している場合は複数にもなる)

```plantuml
hide circle
hide field
hide method
" ユーザ " ||..|| "1" " 氏名 "
" ユーザ " ||..o| "0..1" " 生年月日 "
" ユーザ " ||--|{ "1..*" " メールアドレス "
" ユーザ " ||--o{ "0..*" " 所属組織 "
```

テストケースでは、各データが0の場合、1の場合、複数の場合を作成します。
0を許容する場合には、0件（データ無し）の場合にもエラーにならないことを確認します。
複数を許容する場合には、複数件の場合にもエラーにならず、複数のデータが登録・表示などされることを確認します。

多重度に関するバグは、要件定義・仕様策定・設計・製造のいずれのフェーズにおいても混入します。
担当者の経験不足による考慮漏れや、担当者間で認識に齟齬があったりすると発生する、典型的なバグの1つです。

テストから話は逸れますが、仕様書・データ項目定義書・インターフェース仕様書などには必須かどうかや複数になることがあるかどうかを明記しておくことが重要です。
プログラミング言語の型機能やnullセーフ機能を利用することでもこの種のバグを減らすことができます。
また、0件の場合、プログラムの変数やDBのカラムがnullとなるのか、文字列であれば空文字とするのか、配列やコレクションであれば要素数を0とするのか、そのあたりもきっちり決めておきましょう。


## 境界値に着目する

これは一般的なテスト技法ですので、以下の記事などを参考にしてください。

* [同値分割・境界値分析の解説 - Qiita](https://qiita.com/softest/items/648d8bb4021cd1256b02)


## データ項目の網羅

ほとんどのプログラムでは、データの受け渡し処理が存在します。
例えば、HTTPリクエスト・レスポンスのデータ項目とオブジェクトのマッピング、ビジネスロジックの引数に渡すデータ、データベースの登録・読み取り時のO/Rマッピングなどです。

![data_delivery.dio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27823/8733a2c3-74bc-b2fe-815e-eb38faea4c84.png)

このときに、漏れや間違いが発生することはよくあります。
O/Rマッパーなどのフレームワークを使ったり、コード生成ツールを使ったりするとバグの発生確率を減らすことができるかもしれませんが、ゼロにはできません。
やはり、すべての項目が正しく受け渡されていることを確認するテストを行ったほうがよいです。

このようなテストを実施するタイミングは、システム全体（画面、外部API、DB等）がつながったときの結合テストやシステムテストでよいと思います。
単体テストで確認したとしても結合したときに正しく動く保証はなく、二度手間になるからです。

このときのテストデータは、項目を識別できるものが望ましいです。
例えば、画面フォームから入力した「姓」と「名」の項目が、逆に登録されてしまうというようなバグはよくあります。
「姓」と「名」の入力値を共に「ああああ」にしてしまうと入れ替わったことに気付くことができません。

![input_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27823/eb71107b-3498-37f2-9f86-fe80220b852d.png)

このときに、データ項目ごとの最大文字数/桁数のテストや、文字種のバリエーションのテストも合わせて実施するとよいでしょう。
ここでは、最大文字数/桁数のデータがデータベースに格納できるかや、画面表示にレイアウト崩れが発生しないかを確認します。
前述の「[データの多重度に着目する](#データの多重度に着目する)」と重複しますが、オプション項目（入力が必須ではない項目）については、未入力の場合にエラーが発生したりレイアウト崩れが発生したりしないことも確認します。


## 外部連携のテスト

Web APIなど、外部のサービスを呼び出して使う場合について記載します。

呼び出し元のテストケースとしては、「[データ項目の網羅](#データ項目の網羅)」の内容と、インターフェース仕様書に定義されたエラーケースを網羅していればよいでしょう。
インターフェース仕様書の記載に曖昧な点がある場合は、サービスの提供元に確認をとってください。（データ項目のフォーマット、必須かどうか、異常時に何が返ってくるかなど）

このようなテストをするときの心構えとして、「最初はテストが失敗して当然、成功したらラッキー」ぐらいに思っておくとよいです。
最初からうまく動いて当然だと思っているとバグを見落としやすくなります。また、動かないときストレスになるかもしれません。

うまく動かない理由はさまざまです。
相手側の実装がまずい場合もあるし、自分側の実装に問題がある場合もあります。
仕様の認識の齟齬がある場合や、ネットワークなどの環境が原因かもしれません。

相手側の問題であり、「修正しました」と連絡を受けた場合、直っていることを必ず自分の目で確かめてください。
相手を信用して再テストを実施しないということをしてはいけません。


## UIのテスト

UIと言ってもさまざまですので、汎用的なことを少し記載します。

動的に値の変わる箇所が最大文字数/桁数の場合にもはみ出たり隠れたりしないことを確認します。

システムがサポートするブラウザや端末（画面サイズ）のバリエーションでテストをします。
レイアウト崩れだけではなく、JavaScriptやUIパーツがサポートされないために動かなかったり表示されなかったりすることもありえます。

二度押し防止制御が正常に動くことも確認したほうがよいです。
画面の見た目では制御できているのか分かりづらい場合は、ボタンクリック時などに実行される処理でログを出すようにすると簡単に分かります。


## 状態・手順の組み合わせからテストケースを作る

Webアプリやスマホアプリのような画面操作のテストを行なう場合、まずは状態・手順のバリエーションを洗い出し、それらを組み合わせてテストケース（テストシナリオ）を作っていきます。

洗い出しにはマインドマップを使う方法などもあるようですが、筆者の場合はテキストファイルに箇条書きで書いていくことが多いです。
以下に例として、ECサイトの商品購入フローのテストケースの元ネタとなる状態・手順を記載します。

```
システム設定
  外部決済
    有効
    無効
ユーザ環境
  PC
  スマートフォン
ログイン状態
  ログアウト
  ログイン
  セッション有効期限切れ
カートアイテム
  なし
  1件
  複数件
  前回ログイン時のカートアイテム
    あり
    なし
  ゲスト時のカートアイテム
    あり
    なし
会員種別
  ゲスト
  一般
  ゴールド
在庫
  あり
  なし
決済方法
  代引き
  クレジット
  外部決済
  .
  .
  .
```

このときも、データの[多重度](#データの多重度に着目する)や[境界値](#境界値に着目する)に着目してバリエーションを列挙するとよいでしょう。

バリエーションの組み合わせは膨大な数になることが多いので、すべての組み合わせのテストはできません。
機能の網羅性や重要度を考慮したり、バグが出そうなものを中心にいくつか選ぶことになります[^combination_test]。

[^combination_test]: 組み合わせを作る方法として、直交表やオールペア法などが知られています。


## 単性能試験

性能検証を適切に実施するのはとても手間がかかります。
Webシステムであれば、本番相当のサーバを立て、多量のテストデータを作成し、テストスクリプトを作って多数のクライアントから同時にリクエストを送る必要があります。
このようなテストのやり方については他の記事に譲ります。

まずは、性能問題が出そうな箇所の単体性能測定をするべきです。
測定対象がメソッド（関数）であれば、呼び出しの直前と直後のシステム時間の差異を測定します。
HTTPリクエストであれば、リクエストを送信してからレスポンスを受信するまでの時間を測定します。

単性能試験でよい結果が出たからといって、本番運用に耐えられるという保証にはなりません。
しかし、単性能試験の結果が悪かった場合、本番運用には到底耐えられないでしょう。
今の実装や処理方式では全然駄目ということが早期に分かれば、手戻りを少なくできます。


## 開発者の思い込み

開発者や設計者の間違った思い込みから発生するバグがあります。
他人が作ったものでも自分がつくったものでも、その観点でテストケースを考えてみるとよいでしょう。

例をいくつか示します。

* 「このデータ項目は画面から入力するときにバリデーションを行っているので、必ずこのフォーマットになっているはず」と思い込んでいても、そのバリデーション機能が実装される前に登録されたデータはそうとは限らない
* 「ユーザ登録時に関連する○○○データが一緒に登録されるので、○○○データは必ず存在する」と思い込んでいても、旧システムから移行してきたデータについてはそうとは限らない
* 「削除機能を実行すると、関連するデータがすべて削除される」と思い込んでいても、一部のデータは残っていて同一のIDで再登録すると重複エラーになる


## 障害レポートを受け取ってからやること

障害レポートに記載された症状やエラーログから、「あれが原因だろう」とすぐ分かることもあります。
このような場合でも、まずやるべきことは再現確認です。再現確認をする前に修正に着手してはいけません。
原因と思っていたものが勘違いだったり、障害レポートの記述が正確ではないこともありえます。特定のバージョンや環境でしか再現しない問題かもしれません。
修正してテストをして障害が出ないので「これでOK！」と思っていても、無意味なことをやっていたということになりかねません。

必ず以下の順序を守りましょう。

1. 障害を再現させる
1. 修正する
1. 障害が再現しなくなったことを確認する