*

Spring5入門[STS(Spring Tool Suite)で簡単なWebアプリの典型的なユニットテストの実現方法]

公開日: : 最終更新日:2017/06/26 Spring , , ,


スポンサードリンク



前回は「Spring入門[STS(Spring Tool Suite)の環境作成と簡単なWebアプリの作成]」で、Spring MVCを利用した簡単なWebアプリの説明をさせていただきました。

今回は前回の内容の続きで、Webアプリの典型的なユニットテストの実装方法を説明させていただきます。
出発点は
「Spring入門[STS(Spring Tool Suite)の環境作成と簡単なWebアプリの作成]」が終了した状態となりますので、まずはこちらをご覧ください。

前回はログイン画面であるにもかかわらずログインできていない状態でしたので、今回はログイン処理の実装を行います。

内容は以下の通りです。

  • 本エントリーの概要
  • Dao、Service,Controllerのテスト
  • アプリケーションサーバからインメモリモードのh2 databaseを利用

1. 本エントリーの概要

やはりシステム開発は楽しくなくといけないですよね、そのためのアジャイルでありテストの自動化だと思います。
でもよくありがちなのが、ユニットテストを導入しているけど特定の人しか動かさないし、動かす時にはいっぱいテスト通らず結局はメンテできなくなり、悲しい結果になる。みたいな事が多いと思います。

そうなる理由は色々あるとは思うのですが、一番の打開策は、テストが誰でも簡単に実行できることである考えています。
テストが簡単に実行できて、作業者が自身の幸福に寄与していると感じられればユニットテストはとても有効です。

テスト実行しようとしても
テストを実行する環境が・・・
接続先のDBが・・・
接続先のDBのテーブル定義が古い・・・
メンテされていない・・・
などなど、メンテされていないのは簡単に実行できないことから発生している副次的な理由であると思います。

という流れから組み込みのデーターベースの利用を激しくおすすめいたします。
テスト実行するとDBが起動し、DBのテストが実行できる。
インメモリなので実行速度も格段に速いです。

また、これは自動テストとは関係ないのですが、実装フェーズではアプリケーションサーバで動作するWebアプリも組み込みのデーターベースを利用する構成とすることで生産性の向上が図れると考えています。まあできるだけ手作業での作業は行わずにユニットテストで確認することが前提とはなるとは思います。

組み込みデーターベースでも、SQL方言を指定して起動することができるものも存在しますので、実装フェーズでは問題ないと思いますし、
CIツールを利用して、実装フェーズ期間でも、実際に利用するデーターベースを利用してユニットテストを実行していれば問題点も1日のタイムラグぐらいで検出できます。

本エントリーでは「H2 Database Engine」をインメモリモードで、SQL方言はPostgreSQLで利用いたします。

DBアクセスはSpringで最もシンプルな手段であるSpring JDBCを利用します。

2. Dao、Service,Controllerのテスト

build.gradleの依存関係への追加

build.gradleの依存関係にH2 Database(16行目)とSpring JDBC(18行目)とを追加します。
繰り返しとなりますが、プロジェクトの中身は
「Spring入門[STS(Spring Tool Suite)の環境作成と簡単なWebアプリの作成]」終了時点のものとなります。

com.h2databaseは現時点ではtestCompileでも問題ないのですが、最終的にはSTSの「Pivotai tc Server」からも利用するのでcompileとして定義しています。

パッケージエクスプローラのSpringMvcSampleを選択した状態で右クリックメニューを表示し、
[Gradle(STS)]-[Refresh Dependencies]をクリックします。

Springの設定ファイル(contextConfigLocation)の変更

H2 Databaseのデーターソースを定義とSpring JDBC用の定義を追加します。

29行目のurlに;MODE=PostgreSQLを付与して、H2 DatabaseをPostgreSQLモードで動作させるようにしています。これによってH2 Databaseだけど、PostgreSQL互換で動作させることができます。

37行目のclasspath:/data/sql/init.sqlの内容は以下のとおりです。

H2 Databaseをインメモリモードで動かすとDBへのコネクションを生成した時点でDBが起動し、
DBコネクションが閉じられた時点でDBも停止しますので、コネクションをまたいだデータの永続化ができません。

37行目の記載でH2 Databaseが起動時に指定したSQLを実行してくれますので
利用するテーブルの作成用SQLや必要な初期データを生成するためのSQLを準備しておく必要があります。
厳密には、この定義を行わず、テスト開始時にテーブルも含めて前提データとしてセットアップするとの方法もあります。
実際に格納場所はsrc/main/resources/data.sql/init.sqlとしています。
これは先ほど説明させていただいたとおりで、STSの組み込みのWebサーバからも参照することを考慮しているからです。

Daoの実装

ログイン画面ですので、Userテーブルを検索する必要があります。
今回はエンティティは利用せずにorg.springframework.jdbc.core.JdbcTemplateのList>を返却するqueryForListメソッドを利用します。

UserDao

UserDaoは以下のようになります。usersテーブルのuser名を指定して検索し、結果のListを返却しています。

14,15行目のjdbcTemplateの定義なのですが、contextConfigLocationで指定したBeanをインジェクションするとの指定を@Autowiredを利用して行っています。なおBean定義ではidを明示的に指定いませんが、指定しないときのClass.class.getSimpleNameの一文字目を小文字に変換した文字列となります。

よって、org.springframework.jdbc.core.JdbcTemplateの場合はidはjdbcTemplateとなり、変数名jdbcTemplateには
org.springframework.jdbc.core.JdbcTemplateがインジェクションされます。

UserDaoTest

UserDaoTestは以下のようになります。

userDao.selectByNameに対して、DBに存在するname指定時のテストと、DBに存在しないname指定時のテストとなります。
ってそのままですね・・・

テストケース毎にDBに前提データを設定するように変更

現時点では、UserDaoに単一メソッドしか存在していないのでこれでいいのですが、メソッドが増えてきたり、テストのバリエーションが増えてきた場合は前提データのセットアップの点で難があります。

/data/sql/init.sqlからinsert文を除き、テストケースの実行前にテストで利用する前提データをセットアップするようにしてみます。
実際の開発では、init.sqlでテーブルの作成とテストで共通に利用するデータの投入を行い
個別テストケースで必要なデータのみテスト実行時にセットアップするとの構成が変更に強いテストデータの構成と言えますが
今回は個別テストケースの前提データのみを利用します。

init.sqlの変更

init.sqlのinsert文を取り除きます。
/data/sql/init.sql

この時点でUserDaoTestを実行すると、予想通り「testSelectByName_存在するname指定時」は失敗します。

spring-test-dbunitの依存関係を追加

build.gradleにspring-test-dbunitとDbUnitの依存関係を追加します。

テスト前提データの作成

src/test/resources/META-INF/testdata/xml/users/users_一件.xmlを作成します。
中身は以下のようになります。

/data/sql/init.sqlから除去したinsert文の内容をDbUnitのxml形式に変換しただけとなります。

UserDaoTestの修正

・24-28行
spring-test-dbunitを利用するためのアノテーションを付与しています。

・34,44行
@DatabaseSetupアノテーションでDBにセットする前提データを指定しています。

他はimport文が増えただけでテストケースの内容自体は変更しておりません。
テストを実行すると成功しましたのでDaoのテストはこれで完了となります。

Serviceの実装

LoginServicdを実装します。

UserDaoのselectByNameを呼び出して、結果のリストにパスワードが一致する要素が存在するか場合にtrueを返却しているだけです。
業務ロジックとしてはツッコミどころ満載ですが・・・、例としてはシンプルで分かりやすいと思います。

ServiceTestの実装

テストは以下のようになります。

うんうん・・・
なんでやねん!とのツッコミ期待してます。

確かに結合テストとしてはありなのですが、単体(ユニット)テストとしてはこのコードはNGです。

クラスやtestLoginメソッドに付与されているアノテーションがUserDaoTestと全く同じですよね
これはLoginServiceのテストがUserDaoの実装に依存している事を意味しています。

これではテストが失敗した時にUserDaoの不具合なのか、LoginServiceの不具合なのかが分かりません。
UserDaoをモックにして、LoginServiceのユニットテストに書き換えてみます。

変更点のポイントとしては
・@TestExecutionListenersと@DatabaseSetupの除去
理由は先ほども記載させていただきましたがDBUnitを利用する必要がないからです。

・loginSeviceのアノテーションを@Autowiredから@InjectMocksに変更
LoginSeviceのインスタンスメンバであるuserDaoをモック化するために@InjectMocksを付与する必要があります。

・@Mockを不要したインスタンスメンバのuserDaoを宣言
これによってuserDaoをモック化できます。ただこの記載だけでは32行目で宣言しているloginSeviceインスタンスが保持しているuserDaoをモック化する事はできません。これをモック化するために
41行目の
MockitoAnnotations.initMocks(this);
が必要となります。

最後にorg.mockito.Mockito.whenでモック化したuserDaoのselectByName(“testUser1”)の結果をセットします。
これによってテスト実行時のuserDaoの振る舞いを固定化できます。

テストを実行してみると成功しましたので
ログイン失敗時のテストも必要ですので追加しようと思ったのですが、モックの振る舞いをセットする部分は、ログイン失敗時もほぼ同様の処理となりますで、まずは共通化してみます。

テストを実行すると成功しました。よしよしこれで”testLogin_パスワードが異なる時”テストケースが簡単に実装できます。
“testLogin_パスワードが異なる時”は以下のようになりました。

実行してみると、成功・・・しませんでした。

LoginService.loginの
if(matchedUserOptional.get() != null) {
でjava.util.NoSuchElementException: No value presentが発生して落ちてしまいました。

存在しない時のチェックの方法が誤っていますね・・・
修正後のLoginServiceは以下のとおりです。

再度テストを実行すると”testLogin”、”testLogin_パスワードが異なる時”ともに成功しました。

LoginControllerの修正

前回エントリー終了時のLoginControllerは以下のようになっていました。

詳細につきましては、「Spring入門[STS(Spring Tool Suite)の環境作成と簡単なWebアプリの作成]」を参照ください。

概要としては
コンテキストルート/login/index/にアクセスするとwebapp/WEB-INF/vies/jsp/login/index.jsp
のログイン画面が表示されます。

ユーザ名とパスワードを入力してバリデーションに成功すると
webapp/WEB-INF/vies/jsp/login/success.jspが
失敗すると
webapp/WEB-INF/vies/jsp/login/index.jsp
がそれぞれ表示されます。

前回エントリー終了時のLoginControllerTestは以下のようになっていました。

復習が長くなっておりますが、LoginController.loginから先ほど作成したLoginService.loginを呼び出すようにします。

LoginControllerTestを実行すると”loginの正常系”だけ失敗しました。

既存のテストケースでLoginServiceが呼び出されるのは”loginの正常系”だけですので納得の結果です。

“loginの正常系”が成功するように修正します。

・41-47行
LoginServiceTestと同様にテスト対象クラスのLoginControllerに@InjectMocksを付与してインスタンス変数を宣言し
テスト対象クラスが呼び出すクラスであるLoginServiceに@Mockを付与してインスタンス変数を宣言します。

・52,54行
42行目のMockitoAnnotations.initMocks(this);はLoginServiceTestと同様で自身のインスタンス変数の@Mockをモック化する処理となります。
Controllerのテストで異なる部分が54行目で、この処理によりMockMvcが利用するLoginControllerのインスタンスのメンバのloginServiceがモック化されます。

・71行
モック化したloginService.login(“1234567890”, “1234567890123456”)がtrueを返却するように振る舞いを定義しています。
これによって74,75行の処理が成功します。

・77行
LoginServiceTestの時はあえてチェックしていなかったのですが、期待すべきモックの呼び出しが行われた事を検証しています。
loginService.login(“1234567890”, “1234567890123456”)が1回だけ呼ばれたことを確認しています。

最後に”login異常系_アカウント情報が誤っているとき”のテストケースを実装いたします。

loginService.loginの戻り値がfalse
遷移先のパスがlogin/indexに変わっただけで本当代わり映えしない内容ですよね・・・
全体的に共通化してみました。

引数が多くてきれいではないですね・・・
Javaにはタプルが存在しないのでインナークラスを利用して引数をまとめた方がきれいですね・・・

3. アプリケーションサーバからインメモリモードのh2 databaseを利用

最後にSTSの組み込みサーバである「Pivotai tc Server」からH2 Databaseを利用する方法を説明させていただきます。

org.h2.server.web.DbStarterを継承したクラスの作成

アプリケーションサーバ起動時にコネクションを作成して、サーバ起動中はDBが継続動作してくれる仕組みを実現するために利用するのがorg.h2.server.web.DbStarterです。

このクラスを継承したクラスを作成し、contextInitializedメソッドをオーバーライドすることでSQLを実行できます。
このクラスをweb.xmlでリスナーとして登録し、context-paramで設定した値で動作させることが可能です。
実行するSQLもcontext-paramで指定します。

今回作成したクラスは以下のとおりです。

org.h2.server.web.DbStarterをweb.xmlにリスナーとして定義

web.xmlは以下のようになります。

db.urlにはcontextConfigLocation.xmlで定義したdataSourceのURLとほぼ同じなのですが
;DB_CLOSE_DELAY=-1は付与しておりません。;DB_CLOSE_DELAY=-1を付与しなくてもorg.h2.server.web.DbStarterがコネクションを保持してくれるからです。

DB起動時に発行するSQLは
/data/sql/init.sqlと/data/sql/initdata.sqlを指定しています。
init.sqlは前に記載させていただいたファイルで、table作成
initdata.sqlは

とテストデータのインサート用SQLとなります。

「Pivotai tc Server_を起動すると以下のようなログが出力され、SQLが実行されている事がわかります。

実際にブラウザで
http://localhost:8080/SpringMvcSample/login/login
にアクセスしてtestUser1/testUser1Passを指定してログインするとログインできました。
これで、gitやsvnからプロジェクトをチェックアウトしてすぐにユニットテストの実行や、ブラウザでの動作確認が行える環境が作成できるようになります。

「Spring5入門[STS(Spring Tool Suite)で簡単なWebアプリの典型的なユニットテストの実現方法]」は以上となります。


スポンサードリンク



関連記事

Spring5入門[STS(Spring Tool Suite)の環境作成と簡単なWebアプリの作成]

Struts1ももう過去の遺物になり、SAStrutsもEOLとなりもう半年以上が経過しました。

記事を読む

Message

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

Spring5入門[STS(Spring Tool Suite)で簡単なWebアプリの典型的なユニットテストの実現方法]

前回は「Spring入門」で、Spring MVCを利用した簡単なWe

Spring5入門[STS(Spring Tool Suite)の環境作成と簡単なWebアプリの作成]

Struts1ももう過去の遺物になり、SAStrutsもEOLとなりも

Selenium入門その6[Selenium3でWebDriver(Java/Junit4)の環境を作成しEdge,Chrome,Firefoxで確認してみる]

Selenium3も3.0.1がリリースされましたし、今後は本格的にS

Selenium利用時のトラブルシューティング方法[クリック編]

Seleniumは便利なテスト自動化ツールですし、今後は更なる利用者の

Java8のラムダ式とStream APIを利用してコーディング量の削減サンプル集

Java8になりラムダ式と「Stream API」が利用できるようにな

→もっと見る

Optimization WordPress Plugins & Solutions by W3 EDGE
PAGE TOP ↑