Rust 始めました
MyBatis + Guice でカンタン DAO 実装(その 4)
View の作成
Velocity で作る。
list.vm
<!doctype html> <html> <head> <title>日本の政令指定都市一覧</title> <link rel="stylesheet" href="css/list.css" type="text/css"> </head> <body> <table> <caption>日本の政令指定都市一覧</caption> <colgroup> <col id="no"> <col id="pref"> <col id="name"> <col id="button"> </colgroup> <thead> <tr> <th>No.</th> <th>都道府県</th> <th>都市名</th> <th></th> </tr> </thead> <tbody> #foreach ($city in $citylist) #set ($i = $foreach.index % 2) <tr class="tr$i"> <td>$city.id</td> <td>$city.pref</td> <td>$city.name</td> <td class="button"> <form action="detail" method="post"> <div> <input type="hidden" name="id" value="$city.id"> <input type="submit" value="詳細"> </div> </form> </td> </tr> #end </tbody> </table> </body> </html>
detail.vm
<!doctype html> <html> <head> <title>${city.name}の詳細</title> <link rel="stylesheet" href="css/detail.css" type="text/css"> </head> <body> <table> <caption>${city.name}のデータ</caption> <tr> <th>指定日</th> <td>$date.format('yyyy 年 M 月 d 日', $city.designatedDay)</td> </tr> <tr> <th>地方</th> <td>$city.dstName</td> </tr> <tr> <th>都道府県</th> <td>$city.pref</td> </tr> <tr> <th>面積</th> <td>$number.format('#,###.##', $city.area)km²</td> </tr> <tr> <th>人口</th> <td>$number.format('#,###,###', $city.population) 人</td> </tr> </table> <p><a href="list">一覧に戻る</a></p> </body> </html>
<!doctype html> <html> <head> <title>Error Page</title> </head> <body> <h1>エラー</h1> <p>このページに直接アクセスすることはできません。</p> <p><a href="list">一覧に戻る</a></p> </body> </html>
<!doctype html> <html> <head> <title>Index Page</title> </head> <body> <% response.sendRedirect("list"); %> </body> </html>
MyBatis + Guice でカンタン DAO 実装(その 3)
Servlet の作成
DAO の使い方の例として。
package jp.mydns.akanekodou; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.annotation.WebServlet; import java.util.List; import jp.mydns.akanekodou.bean.City; import jp.mydns.akanekodou.dao.CityDAO; import jp.mydns.akanekodou.dao.util.DaoUtil; @WebServlet("/list") @SuppressWarnings("serial") public class ListServlet extends HttpServlet { @Override protected void doGet( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException { CityDAO dao = DaoUtil.getDao(CityDAO.class); List<City> citylist = dao.all(); request.setAttribute("citylist", citylist); getServletContext() .getRequestDispatcher("/list.vm") .forward(request, response); } }
package jp.mydns.akanekodou; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.annotation.WebServlet; import jp.mydns.akanekodou.bean.City; import jp.mydns.akanekodou.dao.CityDAO; import jp.mydns.akanekodou.dao.util.DaoUtil; @WebServlet("/detail") @SuppressWarnings("serial") public class DetailServlet extends HttpServlet { @Override protected void doGet( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException { getServletContext() .getRequestDispatcher("/error.html") .forward(request, response); } @Override protected void doPost( HttpServletRequest request, HttpServletResponse response ) throws ServletException, IOException { CityDAO dao = DaoUtil.getDao(CityDAO.class); int id = Integer.parseInt(request.getParameter("id")); City city = dao.find(id); request.setAttribute("city", city); getServletContext() .getRequestDispatcher("/detail.vm") .forward(request, response); } }
MyBatis + Guice でカンタン DAO 実装(その 2)
前回の続き。
DAO 層の実装(続き)
まずは DAO クラスであることを明示するためのインターフェースを用意*1する。
package jp.mydns.akanekodou.dao.util; import java.util.List; /** * DAO クラス共通インターフェース * * @author Red cat * @param <T> DAO で扱うモデルクラス * @version 1.0 */ public interface Dao<T> { /** * 全件取得 * * @return 全データのリスト */ List<T> all(); }
このインターフェースを実装して、実際の DAO を作る。
package jp.mydns.akanekodou.dao; import com.google.inject.Inject; import java.util.List; import jp.mydns.akanekodou.bean.City; import jp.mydns.akanekodou.dao.mapper.CityMapper; import jp.mydns.akanekodou.dao.util.Dao; /** * City 用 DAO クラス * * @author Red cat * @version 1.0 */ public class CityDAO implements Dao<City> { @Inject private CityMapper mapper; /** * 全件取得 * * @return City の全データのリスト * @see jp.mydns.akanekodou.dao.util.Dao#all() */ @Override public List<City> all() { return mapper.all(); } /** * 主キーを指定して 1 件取得 * * @param id 主キー * @return 指定した主キーを持つ City のデータ */ public City find(int id) { return mapper.find(id); } }
@Inject
アノテーションが付与されたフィールドは、Guice 経由でインスタンス化するときに設定ファイルに従って自動的にインジェクションされる。
一見して CityMapper
と同じ処理をしていて冗長に見えるが、この方が DAO が SQL マッピングファイルから独立していて扱いやすいし、Guice 向けに見ても都合が良い。
MyBatis + Guice でカンタン DAO 実装(その 1)
O/R マッパーである MyBatis と軽量 DI コンテナ Guice による DAO 実装について、今回から数回に分けて書きます。
MyBatis とは
MyBatis は iBatis の後継となる O/R マッパー。マッピングファイルに SQL を記述し、インターフェースを用意することで、マッピングファイルから自動的にインターフェースの実装をしてくれる。
Guice とは
Google が開発している Java 向けの軽量 DI コンテナ。DI は "Dependency Injection" の略で、日本語に訳すと「依存性の注入」。外部のオブジェクトに依存する部分を、実行時に外部から注入する仕組みで、オブジェクト間の依存関係をソースコードから排除し、オブジェクトの独立性を高め、単体テストをしやすくする。
下準備
今回使用するテーブルについて
データベースは MySQL を使用。
必要なライブラリ
- MyBatis
- mybatis-3.2.5.jar
- Guice*1
- guice-3.0.jar
- javax.inject.jar
- aopalliance.jar
- MyBatis + Guice 連携
- mybatis-guice-3.5.jar
- Velocity*2
- velocity-1.7.jar
- velocity-tools-2.0.jar
- commons-beanutils-1.9.1.jar
- commons-collections-3.2.1.jar
- commons-digester-1.8.1.jar
- commons-lang-2.6.jar
- commons-logging-1.1.3.jar
- JDBC ドライバ
JUnit4 による例外発生時のメッセージのテスト
JUnit 4.7 からの機能で、例外が throw
されたときのメッセージ(getMessage
で取得できる文字列)をチェックすることが出来るようになったらしい。具体的には、同じ例外クラスを throw
するが、ケースによってメッセージが変わるような場合に、@Test(expected = ...)
では確認しきれないので、この方法を使うようである。
実際例
前回の例を修正して、今回の実際例として用いる。
ChangeNum
クラスの修正
package jp.mydns.akanekodou; /** * <p>整数を表す文字列を整数に変換する</p> * * @version 1.1 * @author Red cat */ public class ChangeNum { /** * <p>数値文字列を整数に変換する</p> * * @param str 整数を表す文字列 * @return 変換された整数値 * @throws NumberFormatException 引数の文字列が整数値を表していないとき * @see Integer#parseInt(String) */ public int changeNum(String str) throws NumberFormatException { try { return Integer.parseInt(str); } catch(NumberFormatException e) { // NumberFormatException を catch したら // メッセージつきの NumberFormatException を throw する throw new NumberFormatException("入力された値が整数値を表していません。"); } } }
NumberFormatException
を catch
した際に、エラーメッセージを独自に設定した NumberFormatException
を新たに throw
する仕様に変更している。
例外検証ルールの作成
@Rule
アノテーションと ExpectedException
クラスを利用して検証ルールを作成する。
package jp.mydns.akanekodou.test.junit4; import jp.mydns.akanekodou.ChangeNum; import org.junit.*; import org.junit.rules.ExpectedException; import static org.junit.Assert.*; import static org.hamcrest.core.Is.is; /** * <p>JUnit4 による ChangeNum テスト用クラス</p> * * @author Red cat */ public class ChangeNumTest { private static ChangeNum target; @Rule public ExpectedException thrown = ExpectedException.none(); /** * <p> * テスト前処理<br /> * 全体を通して 1 回だけ実行 * </p> */ @BeforeClass public static void getTestClass() { target = new ChangeNum(); } /** * <p>正常系テスト</p> */ @Test public void success() { String str = "100"; int expected = 100; int actual = target.changeNum(str); assertThat("数値に変換されていません。", actual, is(expected)); } /** * <p>異常系テスト</p> */ @Test public void exception() { String str = "99.9"; // 整数を表していない ! // 例外検証ルールの設定 thrown.expect(NumberFormatException.class); thrown.expectMessage("入力された値が整数値を表していません。"); target.changeNum(str); // ここで NumberFormatException が発生するはず } }
これで期待される例外のみならず、その際に期待されるエラーメッセージも一致したときに限って異常系のテストが成功する。
(参考)JUnit3 による例外発生時のメッセージテスト
JUnit3 の場合はことがもう少し簡単(?)で、一行追加するだけである。
package jp.mydns.akanekodou.test.junit3; import jp.mydns.akanekodou.ChangeNum; import junit.framework.*; /** * <p>JUnit3 による ChangeNum テスト用クラス</p> * * @author Red cat */ public class ChangeNumTest extends TestCase { private ChangeNum target; /** * <p>コンストラクタ</p> * * @param name {@inheritDoc} * @see TestCase#TestCase(String) */ public ChangeNumTest(String name) { super(name); } /** * <p> * テスト前処理<br /> * 各テストメソッドの前に毎回実行 * </p> * * @see TestCase#setUp() */ @Override public void setUp() { target = new ChangeNum(); } /** * <p>正常系テスト</p> */ public void testSuccess() { String str = "100"; int expected = 100; int actual = target.changeNum(str); assertEquals("数値に変換されていません。", expected, actual); } /** * <p>異常系テスト</p> */ public void testException() { String str = "99.9"; // 整数を表していない ! try { target.changeNum(str); // ここで NumberFormatException が発生するはず fail("例外が発生していません。"); } catch(NumberFormatException e) { assertEquals("入力された値が整数値を表していません。", e.getMessage()); } } }
JUnit による単体テストの自動化
JUnit は Java における単体テストの自動化をサポートするサードパーティー製のライブラリ(フレームワーク)である。
が、これだけの説明では何のことかわからないと思うので、もう少し詳しく説明する。
単体テストの定義と意義
単体テストとは ?
単体テストとは、1 個のクラスだけをテストすることである。テストをする項目が複数のクラスにまたがっていないことが条件である。もちろん、テストするクラスの中で別のクラスを呼び出している場合もあるが、それらのクラスに関しては正しく動作するものと仮定してテストを実施する。
具体例を見てみよう。
package jp.mydns.akanekodou; /** * <p>整数を表す文字列を整数に変換する</p> * * @version 1.0 * @author Red cat */ public class ChangeNum { /** * <p>数値文字列を整数に変換する</p> * * @param str 整数を表す文字列 * @return 変換された整数値 * @see java.lang.Integer#parseInt(String) */ public int changeNum(String str) { return Integer.parseInt(str); } }
package jp.mydns.akanekodou; import java.io.InputStreamReader; import java.io.BufferedReader; import java.io.IOException; /** * <p> * メインクラス<br /> * 標準入力から取り込んだ文字列を整数値に変換する * </p> * * @author Red cat */ public class Main { /** * <p>メインメソッド</p> * * @param args コマンドライン引数 */ public static void main(String[] args) { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try { System.out.print("整数を入力してください: "); String str = br.readLine(); ChangeNum cn = new ChangeNum(); int value = cn.changeNum(str); System.out.println(value); } catch(IOException e) { e.printStackTrace(); } } }
機能としては単純であるが、単体テストの意味を理解してもらうためにわざと冗長な書き方をしている。
ここで単体テストと言った場合、ChangeNum
のみをテストすることである。Main
を動かすことなく ChangeNum
のみをテストするなんて可能なのか、と思うかも知れないが、それを可能にしてくれるのが JUnit である。
単体テストの意義
なぜ単体テストを実施する必要があるのかについて考えてみよう。実際の開発においては、工程の関係で全部の機能が実装された状態になるまでにはかなりの時間を要することが普通である。その際、未実装の機能に依存することなく個々の機能をあらかじめテストしておかないと、結合テストでバグが発生した場合に手戻り作業が多くなる*1。そこで、個々の機能に関しては動作が保証されていることをまず確認することによって、結合テストでバグが発生した際の原因究明作業を楽にすることが出来る。これが単体テストの存在意義である。
テストを自動化することのメリット
テストを自動化しておくことは、多くのメリットがある。単体テストでバグを発見して、機能を修正したとき、テストを自動化しておけば、再びそのテストを実行するだけで再テストが可能になる。また、バグを修正する場合でも、テストコードが標本となるため、修正作業が容易になる。テストコードはこれすなわち、単体機能の仕様書なのである。
また、テストコードさえ作成しておけば、誰でも同じテストを実行出来るので、テストコードの作成者にテストの負担が偏ることがない。
また、JUnit の作者によれば
なによりも、テスト中に現れる緑色のバーが満タンになるのを見ると気分が良くなる
そうである*2。