お問い合わせ

第7回 単体テスト編【誰もが一度はつまずくSpring Bootを解説♪】【若手Javaエンジニア向け】

こんにちは。ステックアップアカデミー講師のかびらです。現役のITエンジニアであり、かつAFP保持者として、このチャンネルを通して、ITエンジニアとフリーランスに必要な、ITとお金に関する情報を配信しています。

今回は「誰もが一度はつまずくSpring Bootを解説」シリーズの第7回目です。このシリーズでは、私も含め、誰もが共通してつまずきやすいポイントを、Javaのスキルや経験が浅い、若手エンジニアでも簡単に理解できるよう、極力難しい用語を使わずに解説しています。

今回は、単体テストについて、私の経験上つまずきやすいと感じたポイントをピックアップして解説します。

単体テストの対象整理

まず、単体テストの具体的な解説に入る前に、テスト工程全体をざっくり俯瞰して、それぞれの工程で何を確認すべきか見ていきましょう。

以前の講義でも解説したように、Spring Bootのプログラムの構造は、プレゼン層、アプリ層、ドメイン層、インフラ層といった層に分かれています。この構成を頭に入れたうえで、単体テストでは何をチェックすべきなのかを考えます。

一般的に、単体テストの目的は、「プログラムの小さな塊、いわゆるモジュールやコンポーネントの単位で、プログラムに問題がないか確認すること」です。つまり、こちらのプログラムの構造に当てはめると、各層ごとに動作を確認する必要があります。

ただ、私の考えでは、単体テストの範囲としてはドメイン層とインフラ層だけで十分だと考えています。プレゼン層やアプリ層の動作確認は、結合テストで行えばOKという整理ですね。その理由については、また別の機会に詳しく解説したいですが、簡単に言うと 「プレゼン層やアプリ層の単体テストはコスパが悪いから」ということですね。

ここまでを整理すると、単体テストで確認するのはドメイン層とインフラ層だけ。そして、結合テストでは、プレゼン層からインフラ層まで全部まとめてプログラムの妥当性をチェックする。この整理を前提に、この後の話を進めていきます。

部品の単体テスト

まずは、部品の単体テストでつまづきやすいポイントについて解説します。

一般的に行われる単体テストの例を、上図でご紹介します。左側がメインコードで、右側が単体テストコードです。

メインコードの処理は、PartsBという部品クラスで、その中にgetTextメソッドが存在します。getTextメソッドでは、「引数で渡されたパターン番号に応じて、switch文で返却される文言が変わる」という仕組みになっています。

このgetTextメソッドの単体テストを特に意識せずに作成すると、右側の単体テストコードに示すように、テストパターンごとにテストメソッドを作るという形になります。この方法自体は、パターンが少なければ特に問題ありません。

ただしパターンが膨大になると、その分のテストメソッドが増えすぎてしまいます。その結果として、最終的に「どのようなパターンでテストが行われているのか分かりにくい」という課題がでてきます。

このような課題の有力な解決方法のひとつとして、パラメータライズドテストが挙げられます。パラメータライズドテストは、このような実施すべきテストパターンが多い際に非常に便利です。

パラメータライズドテストとは、テストメソッドは1つだけで、テストの入力値と期待値を別の方法で定義できる仕組みです。

例えば、右側に示すパラメータライズドテストの例では、テストメソッドを1つだけ用意し、そのメソッドが、入力値のパターン番号と期待値を引数で受け取るようになっています。この入力値と期待値を、テスト対象のメソッドに渡し、戻り値を検証することで、適切にテストを行うことができます。

さらに、入力値と期待値の書き方に注目すると、テストパターンの定義には「CSVソースアノテーション」を活用し、CSV形式で設定することで、どのようなテストが行われているのかがひと目で分かりやすくなっています。

このように、パラメータライズドテストはテストメソッドを最小限に抑え、データパターンを追加するだけで、少ないコード量で見た目をスッキリさせることができます。パラメータライズドテストを有効に活用できるケースがあれば、積極的に取り入れてみてください。

リポジトリの単体テスト

次に、リポジトリの単体テストでつまづきやすいポイントについて解説します。ここでは、リポジトリを使って、データベースにアクセスするプログラムの単体テストについて考えていきます。

まず、使用するデータベースですが、本番環境と同じ仕組みを採用する場合、通常は外部のデータベースを使うことになると思います。しかし、外部のデータベースを使って単体テストを実施しようとすると、「そのデータベースが構築されている環境でしかテストが動かせない」という制約が出てきます。つまり、テストを実行する環境が限定されてしまうんですね。

このような課題に対しては、組み込みデータベースを活用するのがおすすめです。組み込みデータベースとは、その名の通り、アプリケーション内に組み込まれているデータベースのことです。

例えば、Spring BootにはH2DBという組み込みデータベースを簡単に構築できる仕組みがあります。上図はH2DBを設定する例です。テスト用のプロパティファイルに、このようにH2DBを設定すれば、テスト時にH2DBを使うことができるようになります。

ここで、組み込みデータベースを使うメリットを3つご紹介します。

✅環境依存がなく、どの環境でもテスト実行が可能😊
✅メモリ上でデータを保持するため、テスト処理が高速😊
✅テストデータのセットアップやリセットが簡単にできる😊

このように、組み込みデータベースは単体テストと非常に相性がいいので、単体テストを作る際はぜひ活用してみてください。

ここからは、実際に組み込みデータベースを活用したリポジトリの単体テストのコード例を紹介します。今回の説明では、データベースアクセスの仕組みにMyBatisを採用しました。

MyBatisについての詳しい解説は省略しますが、ほかのデータベースアクセスの仕組みを使っていても、基本的な考え方は変わりません。MyBatisでリポジトリを実装する場合、こちらのようにリポジトリのインターフェースと、SQLを記述するXMLファイルを用意する形になります。

こちらがリポジトリの単体テストコードです。MyBatisを採用すると、MyBatisのテスト用アノテーションを使うことで、このように簡単に単体テストを作成できます。また、これらのテストでは、先ほど紹介した組み込みデータベースであるH2DBを使用しているため、高速にテストを実行できます。

サービスの単体テスト

ここからは、サービスの単体テストでつまずきやすいポイントについて解説します。皆さんは、サービスの単体テストと聞いて、どのようなイメージを持ちますか?

例えば、上図のサービスクラスのサービスメソッドの単体テストを考えてみましょう。このサービスメソッド内では、部品Aが呼び出されています。本番環境などで動作する場合、通常この部品Aは本物の部品を使用して実行されます。しかし、単体テストでは一般的に、この部品Aを本物の部品を使わずに、モック化してテストすることが多いですね。

ここで、モックを使うメリットとデメリットを整理してみましょう。

モックを使うメリットはいくつかありますが、代表的なのは、「依存関係の影響を受けずに、対象のサービスクラスをテストできる」という点ですね。先ほども話しましたが、単体テストというのは、モジュールやコンポーネントのプログラムの妥当性を確認するフェーズです。そのため、「テスト対象のメソッドに集中し、他の依存関係にある部分は一旦置いておきたい」というのが一般的な考え方です。そこで、モックを活用すると、依存関係の部分を疑似的なプログラムに置き換えることで、サービスメソッドのプログラムの妥当性確認に集中できるわけです。

ただし、モックを使うことにもデメリットはあります。その代表的なデメリットは、「単体テストにかけた労力に対して、バグの摘出率が低い」という点です。つまり、「費用対効果が低い」ということですね。

「労力に対してバグの摘出率が低い」というデメリットについて、実際の処理を見ながら確認してみましょう。

上図にサービスクラスのサービスメソッドがあります。このクラス内では、部品A、部品B、部品Cと、ほかにもたくさんの部品が呼び出されているとします。

ここで、モックを使ったテストでは、それぞれの部品をモック化して、テスターが返却値を指定することになります。このように、モックだらけの単体テストを想像してみてください。このテストで、本当にバグを見つけられると思いますか?

単体テストに限らず、テストの目的はバグを見つけることですよね。でも、このような単体テストの作りだと、「サービスメソッドのテストが正常に動いた!」と満足してしまい、肝心のバグ摘出にはつながらないことが多いんです。だからこそ、私は多少テストが大変でも、単体テストではモックを極力使わない方がいいと考えています。

では、実際の単体テストコードを見ながら確認していきましょう。

上図の左側のプログラムがメインのコードで、右側がテストコードです。メインコードでは、addUserメソッドの中で、partAから返ってきた文言をユーザー名に付与して、ユーザーを登録するという処理が書かれています。

そして、このaddUserメソッドの単体テストとして、右側ではモックを使ってテストを実装しました。partAをモック化し、partAが呼び出されたときに「さん」と返却されるように設定しています。また、userRepositoryも返却値なしでモック化しています。この状態でテストを実行し、検証しようとします。

しかし、データベース登録処理であるuserRepositoryがモック化されているため、実際にデータベースに登録されたデータの妥当性の検証ができません。

つまり、このテストで確認できるのは「処理が正常に終了するかどうか」だけです。
ただ「正常終了するか」だけを確認するテストって、あまり意味がないですよね?

では次に、モックを使わない単体テストを見ていきましょう。上図のテストが、モックを使わない単体テストの例です。

こちらのテストでは、本物のpartAとuserRepositoryを使っているので、実際に登録されたデータをしっかり検証することができます。ここまで丁寧に検証できれば、バグを検知できる可能性も格段に高まりますよね。

ただ、ここでひとつ注意点があります。この手法を採用するのであれば、呼び出し先の部品などがきちんと品質担保されていることが必要です。もし呼び出し先の部品にバグがあった場合、呼び出し元のテストにも影響を及ぼす可能性があるからですね。

だからこそ、この手法では、単体テストの実施順番が重要になってきます。先に呼び出し先の部品などから単体テストを実施し、十分に品質が確保されていることを確認してから、呼び出し元のテスト行う。この順番をしっかり意識することが大切です。

ここまでの話をまとめます。

私が考える、「サービスの単体テストを効果的に進める方針」としては、まず基本的に 「極力モックを使わずにテストする」 ことです。

ただし、例外的に 「単体テストでは実現が難しい処理」 が出てくることもありますよね。
そういう場合に限って、モックをうまく活用するといいでしょう。

例外的にモックを使ったほうがいいケースを、4つ挙げてみました。

✅外部APIの呼び出し
✅メール送信機能の呼び出し
✅実現困難なデータを扱う場合
✅特殊なエラーや例外を発生させたい場合

これらのケースでは、モックを使わないと単体テストの実施が難しくなります。そのため、こうした場面では、積極的にモックを活用しましょう。

さいごに

今回の講義はここまでです。今回は、単体テストでつまずきやすいポイントを解説してきました。次回も、引き続き、テストつまずきやすいポイントを解説します。

今日もまたひとつ賢くなりましたね!次回の講義もお楽しみに!