■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
                      2009年07月19日

    Java総合講座 - 初心者から達人へのパスポート
                  vol.162

                                セルゲイ・ランダウ
 バックナンバー: http://www.flsi.co.jp/Java_text/
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■


[このメールマガジンは、画面を最大化して見てください。]


========================================================
◆ 01.SOAPのアプリケーション(Webサービス)
========================================================


さて前回は、setUp()メソッドの中でデータベースのデータをテストの準備用に
設定する(復元する)ためのコーディングを行っておくことを読者への課題と
しておきましたが、下にその回答例を提示しておきましょう。
なお、単体テストは通常、開発者の単独のPCで行いますから、Tomcatのアプリケー
ション(Axisのアプリケーション)もクライアントのプログラムもデータベースも
すべて同一のPC上で実行するのが普通です。そして、テスト中は、他者や他のプロ
グラムには一切このPCを触らせないようにします。

--------------------------------------------------------
package jp.co.flsi.lecture.webservice.hotel;

import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Vector;
import java.sql.PreparedStatement;
import javax.xml.rpc.ServiceException;
import junit.framework.TestCase;

public class HotelClientTest extends TestCase {
   private RoomReserveInfo roomReserve;

   protected void setUp() throws Exception {
      roomReserve = new RoomReserveInfo();
      // データベースの準備
      Connection conn = null;
      String driver = "com.mysql.jdbc.Driver";
      String url = "jdbc:mysql://127.0.0.1:3306/HOTELDB";
      String dbUser = "root";
      String dbPassword = "rootpass";
      Class.forName(driver);
      conn = DriverManager.getConnection(url, dbUser, dbPassword);
      conn.setAutoCommit(false);
      Vector<PreparedStatement> statements = new Vector<PreparedStatement>();
      statements.add(conn.prepareStatement("DELETE FROM roombook"));
      statements.add(conn.prepareStatement("DELETE FROM booking"));
      statements.add(conn.prepareStatement("INSERT INTO booking ( BOOKNUM, CUSTNAME, ADDRESS, TELNO ) VALUES (1,'奈々志野権兵衛','千葉県市川市なんとか町1-1-1','123-456-7890')"));
      statements.add(conn.prepareStatement("INSERT INTO booking ( BOOKNUM, CUSTNAME, ADDRESS, TELNO ) VALUES (2,'何途可感杜香','東京都中央区なんとか町1-1-1','123-098-7654')"));
      statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (307,'2009-11-30',2,2)"));
      statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (307,'2009-12-01',2,2)"));
      statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (307,'2009-12-02',2,2)"));
      statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-25',1,1)"));
      statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-26',1,1)"));
      statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-27',1,1)"));
      statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-28',1,1)"));
      statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-29',1,1)"));
      for (PreparedStatement ps : statements) {
         ps.executeUpdate();
         ps.close();
      }
      conn.commit();
      conn.close();
   }

   /**
    * 予約済みのデータでは予約できないことのテスト。
    */
   public void testReserveRoom01() {
      roomReserve.setAddress("東京都中央区なんとか町1-1-1");
      roomReserve.setName("何途可感杜香");
      roomReserve.setNumOfLodgers(2);
      roomReserve.setNumOfNights(3);
      roomReserve.setRoomNum(307);
      roomReserve.setStartDate("20091130");
      roomReserve.setTelNo("123-098-7654");
      try {
         assertFalse(HotelClient.reserveRoom(roomReserve));
      } catch (RemoteException e) {
         fail("RemoteException");
      } catch (ServiceException e) {
         fail("ServiceException");
      }
   }

   /**
    * 未予約のデータでは予約できることのテスト。
    */
   public void testReserveRoom02() {
      roomReserve.setAddress("東京都中央区なんとか町2-2-2");
      roomReserve.setName("あぶらかたぶら");
      roomReserve.setNumOfLodgers(2);
      roomReserve.setNumOfNights(3);
      roomReserve.setRoomNum(301);
      roomReserve.setStartDate("20100101");
      roomReserve.setTelNo("123-098-7654");
      try {
         assertTrue(HotelClient.reserveRoom(roomReserve));
      } catch (RemoteException e) {
         fail("RemoteException");
      } catch (ServiceException e) {
         fail("ServiceException");
      }
   }

   /**
    * 宿泊日の指定が規定外の場合はRemoteExceptionが発生することのテスト。
    */
   public void testReserveRoom03() {
      roomReserve.setAddress("東京都中央区なんとか町3-3-3");
      roomReserve.setName("へのへのもへ");
      roomReserve.setNumOfLodgers(2);
      roomReserve.setNumOfNights(3);
      roomReserve.setRoomNum(302);
      roomReserve.setStartDate("20101301");
      roomReserve.setTelNo("123-098-7654");
      try {
         HotelClient.reserveRoom(roomReserve);
         // 下の行が実行されたときはRemoteExceptionが発生していないことになる。
         fail("RemoteExceptionが発生しなければなりません。");
      } catch (RemoteException e) {
         // ここに来たときは、テスト結果はOKなので何もしない。
      } catch (ServiceException e) {
         fail("ServiceException");
      }
   }

}
--------------------------------------------------------

ここでは、最初に

"DELETE FROM roombook"
"DELETE FROM booking"

のSQLを実行することによって、ROOMBOOKテーブルとBOOKINGテーブルのデータを
すべて削除したあと、テスト用の初期データを各テーブルにINSERTし直しています。
つまり、予約関連のテーブルだけデータを入れ直していますが、部屋の情報の
テーブル(ROOMINFO)には一切触っていません。これは、部屋の情報は固定されて
いるもの(予約とは一切関係がなく、変化することがない)と考え、現状のデータ
をそのまま変更することなくテストに使用するものと考えているからです。


さて、このテスト・ケースのプログラムを実行するためには、例によってMySQLの
ドライバー(com.mysql.jdbc.DriverがはいっているJARファイル)をクラスパス
(ビルドパス)に追加しておく必要がありますので、下記のようにして追加して
おきましょう。

パッケージ・エクスプローラー内のJStudySoapClient(プロジェクト)をを右クリック
し、「ビルド・パス」→「ライブラリーの追加」を選択し、「ライブラリーの追加」ウイ
ンドウにおいて「ユーザー・ライブラリー」を選択(クリック)して「次へ」ボタンを
クリックし、「mysql」(以前vol.083で用意しておいたもの)を選択(チェックマークを
入れる)して「終了」ボタンをクリックします。

以上の作業が終わったら、前回と同じくテスト・ケース(HotelClientTest)を実行
(パッケージ・エクスプローラーの中でHotelClientTestを右クリックし、「実行」→
「JUnitテスト」を選択)しましょう。今度は、何度テスト・ケースを実行し直しても
テスト結果はすべてOKになりますね。



ところで、このようにテスト用のデータベースを準備し、テスト・メソッドを用意する
ことは、本格的なアプリケーションで本格的なテストをするときには、多量で複雑な
データを扱うために、大変な作業になるのではないかと思うかもしれませんが、実際
にはかなり自動化することができます。その方法は、のちに上級レベルの説明をする
ときにお話します。



ここで、ちょっとテストのパフォーマンスの観点でこのソース・コードを検討
してみましょう。

このソース・コードだと、setUp()メソッドは、前回お話したように

(1) setUp()メソッド
(2) testReserveRoom01()メソッド
(3) (tearDown()メソッド)
(4) setUp()メソッド
(5) testReserveRoom02()メソッド
(6) (tearDown()メソッド)
(7) setUp()メソッド
(8) testReserveRoom03()メソッド
(9) (tearDown()メソッド)

のようにテスト・メソッドを実行するたびに毎回実行されることになります。
上のソース・コードでは、setUp()メソッドにおけるデータベースのデータ設定は
最初の1回だけ行えば済むはずのものであり、テスト・メソッドごとに毎回実行
するのは時間の無駄です。ましてや、多量のテスト・メソッドが実行される本格的
なテストの場合は、このような時間のロスも大きくなり、テストのパフォーマンス
を大きく落としてしまいます。

この時間のロスを無くすためには、複数のテストを実行する前にsetUp()メソッドが
1回だけ実行されるような仕組みがあるといいのですが、まさにその仕組みを提供する
ものとして、TestSetupというクラスが用意されています。
このTestSetupを使って、setUp()メソッドを最初の1回しか実行しないようにする方法
を、以下にコーディング例を提示して説明しておきます。(tearDown()メソッドも同様に
最後に1回だけ実行するようにすることができます。)

では、HotelClientTestを下記のように変更してみて下さい。

--------------------------------------------------------
package jp.co.flsi.lecture.webservice.hotel;

import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Vector;
import java.sql.PreparedStatement;
import javax.xml.rpc.ServiceException;
import junit.extensions.TestSetup;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

public class HotelClientTest extends TestCase {
   private RoomReserveInfo roomReserve;

   public static Test suite() {
      TestSetup setup = new TestSetup(new TestSuite(HotelClientTest.class)) {
         protected void setUp() throws Exception {
            Connection conn = null;
            String driver = "com.mysql.jdbc.Driver";
            String url = "jdbc:mysql://127.0.0.1:3306/HOTELDB";
            String dbUser = "root";
            String dbPassword = "rootpass";
            Class.forName(driver);
            conn = DriverManager.getConnection(url, dbUser, dbPassword);
            conn.setAutoCommit(false);
            Vector<PreparedStatement> statements = new Vector<PreparedStatement>();
            statements.add(conn.prepareStatement("DELETE FROM roombook"));
            statements.add(conn.prepareStatement("DELETE FROM booking"));
            statements.add(conn.prepareStatement("INSERT INTO booking ( BOOKNUM, CUSTNAME, ADDRESS, TELNO ) VALUES (1,'奈々志野権兵衛','千葉県市川市なんとか町1-1-1','123-456-7890')"));
            statements.add(conn.prepareStatement("INSERT INTO booking ( BOOKNUM, CUSTNAME, ADDRESS, TELNO ) VALUES (2,'何途可感杜香','東京都中央区なんとか町1-1-1','123-098-7654')"));
            statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (307,'2009-11-30',2,2)"));
            statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (307,'2009-12-01',2,2)"));
            statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (307,'2009-12-02',2,2)"));
            statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-25',1,1)"));
            statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-26',1,1)"));
            statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-27',1,1)"));
            statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-28',1,1)"));
            statements.add(conn.prepareStatement("INSERT INTO roombook ( ROOMNUM, DATE, BOOKNUM, LODGNUM ) VALUES (308,'2009-12-29',1,1)"));
            for (PreparedStatement ps : statements) {
               ps.executeUpdate();
               ps.close();
            }
            conn.commit();
            conn.close();
         }

//         もし、tearDown()メソッドが必要なら、下記のようにコーディングする。
//         protected void tearDown() throws Exception {

//         }
      };
      return setup;
   }

   protected void setUp() throws Exception {
      roomReserve = new RoomReserveInfo();
   }

   /**
    * 予約済みのデータでは予約できないことのテスト。
    */
   public void testReserveRoom01() {
      roomReserve.setAddress("東京都中央区なんとか町1-1-1");
      roomReserve.setName("何途可感杜香");
      roomReserve.setNumOfLodgers(2);
      roomReserve.setNumOfNights(3);
      roomReserve.setRoomNum(307);
      roomReserve.setStartDate("20091130");
      roomReserve.setTelNo("123-098-7654");
      try {
         assertFalse(HotelClient.reserveRoom(roomReserve));
      } catch (RemoteException e) {
         fail("RemoteException");
      } catch (ServiceException e) {
         fail("ServiceException");
      }
   }

   /**
    * 未予約のデータでは予約できることのテスト。
    */
   public void testReserveRoom02() {
      roomReserve.setAddress("東京都中央区なんとか町2-2-2");
      roomReserve.setName("あぶらかたぶら");
      roomReserve.setNumOfLodgers(2);
      roomReserve.setNumOfNights(3);
      roomReserve.setRoomNum(301);
      roomReserve.setStartDate("20100101");
      roomReserve.setTelNo("123-098-7654");
      try {
         assertTrue(HotelClient.reserveRoom(roomReserve));
      } catch (RemoteException e) {
         fail("RemoteException");
      } catch (ServiceException e) {
         fail("ServiceException");
      }
   }

   /**
    * 宿泊日の指定が規定外の場合はRemoteExceptionが発生することのテスト。
    */
   public void testReserveRoom03() {
      roomReserve.setAddress("東京都中央区なんとか町3-3-3");
      roomReserve.setName("へのへのもへ");
      roomReserve.setNumOfLodgers(2);
      roomReserve.setNumOfNights(3);
      roomReserve.setRoomNum(302);
      roomReserve.setStartDate("20101301");
      roomReserve.setTelNo("123-098-7654");
      try {
         HotelClient.reserveRoom(roomReserve);
         // 下の行が実行されたときはRemoteExceptionが発生していないことになる。
         fail("RemoteExceptionが発生しなければなりません。");
      } catch (RemoteException e) {
         // ここに来たときは、テスト結果はOKなので何もしない。
      } catch (ServiceException e) {
         fail("ServiceException");
      }
   }

}
--------------------------------------------------------

変更したことは、suite()というメソッドを追加したことと、以前のsetUp()メソッドの
中のデータベース処理部分をこのsuite()というメソッドの中に移動したことです。

このソース・コードの中に出てきたTestSuiteというのは、複数のテスト・ケースを
一まとめにテストしたりするために用意されたクラスで、詳しいことは、のちの上級
レベルの説明のときにお話いたします。
ここでは、テスト・スイート(TestSuite)にHotelClientTestだけを含ませるように
コーディングしてあります。(具体的には、TestSuiteのコンストラクターの引数に
HotelClientTest.classを指定することによって行っています。)

そして、TestSetupのsetUp()メソッドはテスト・スイート(TestSuite)に含まれる
全テスト・ケースの実行前に1回だけ実行され、TestSetupのtearDown()メソッドは
テスト・スイートに含まれる全テスト・ケースの実行後に1回だけ実行されます。
上記のソース・コードでは、無名クラス(vol.009参照)としてTestSetupのサブクラス
を作り、そこにsetUp()メソッドを実装しています。

その無名クラスのインスタンスをsetupという変数に代入していますが、TestSetupは
Test型(Testはインターフェース)であり、suite()メソッドの戻り値としてsetupを
returnしています。

このように、suite()というメソッドを実装しておくと、JUnitのフレームワークは
このsuite()メソッドを見つけて実行し、その戻り値であるTestオブジェクトを取り
出して実行すべきメソッドを調べだし、順番に実行していきます。
つまり、TestSetupのsetUp()メソッドを実行し、次にテスト・スイートに含まれる
テスト・ケースを順番に実行し、最後にTestSetupのtearDown()メソッドを実行します。

suite()メソッドが実装されていない場合は、現在のテスト・ケースが実行されるだけ
になります。ここでテスト・ケースの実行というのは、前回説明した

(1) setUp()メソッド
(2) testReserveRoom01()メソッド
(3) tearDown()メソッド
(4) setUp()メソッド
(5) testReserveRoom02()メソッド
(6) tearDown()メソッド
(7) setUp()メソッド
(8) testReserveRoom03()メソッド
(9) tearDown()メソッド

というような順序で各テスト・メソッドを実行することを意味します。

したがって、上記のソース・コードのようにTestSetupのsetUp()メソッドとテスト・ケース
のsetUp()メソッドの両方が実装されている場合は、TestSetupのsetUp()メソッドが最初
に1回だけ実行されるとともに、テスト・ケースのsetUp()メソッドはテスト・メソッドごと
に毎回実行されるということになります。



(次回に続く)


では、今日はここまでにします。



┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
★ホームページ:
      http://www.flsi.co.jp/Java_text/
★このメールマガジンは
     「まぐまぐ(http://www.mag2.com)」
 を利用して発行しています。
★バックナンバーは
      http://www.flsi.co.jp/Java_text/
 にあります。
★このメールマガジンの登録/解除は下記Webページでできます。
      http://www.mag2.com/m/0000193915.html
★このメールマガジンへの質問は下記Webページにて受け付けて
 います。わからない所がありましたら、どしどしと質問をお寄
 せください。
      http://www.flsi.co.jp/Java_text/
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Copyright (C) 2009 Future Lifestyle Inc. 不許無断複製