■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
                      2009年07月12日

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

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


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


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


このテスト・ケース(HotelClientTest)は、HotelClientTestという名前が
示す通り、本来はHotelClientのテストという意味を持たせられているの
ですが、今回はHotelClientを通してWebサービスのテストを行うために
使用しています。

では、とりあえず現時点での(自動生成された)HotelClientTestのソース・
コードを見てください。(念のために下に提示しておきます。)

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

import junit.framework.TestCase;

public class HotelClientTest extends TestCase {

   protected void setUp() throws Exception {
      super.setUp();
   }

   public void testReserveRoom() {
      fail("まだ実装されていません。");
   }

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

まず、HotelClientTestはTestCaseのサブクラスとして作られていることがわかりま
すね。そして、import文を見てみるとこのTestCaseのパッケージの名前が
junit.frameworkとなっていることがわかります。
このパッケージ名からもわかるように、JUnitというのはフレームワーク(framework)
です。
つまり、単体テストのための骨組みが予め用意されていて、あとは肉付けをすれば
単体テストのプログラムが完成することになります。

そしてJUnitでは、必要な肉付けは、主に下記の3種類の作業になります。

(1) テストの前準備を行うメソッドを実装する。これはsetUp()というメソッドです。

(2) テストを行うメソッドを実装する。これはtestXXXX()というメソッドで、このメソッ
ド名のうち、XXXXの部分は自分で自由に名前を付けられます。なお、このメソッドを
「テスト・メソッド」と呼びます。

(3) テストの後始末を行うメソッドを実装する。これはtearDown()というメソッドです。

これらのうち、(1)と(3)は必要なければ実装しなくてもかまいませんが、(2)は必ず
実装しなければなりません。(2)が実際にテストを行うためのメソッドですから、
(2)を実装しないことにはテストにならないからです。

なお、テスト・メソッドを複数作りたい場合(というか複数作るのが普通なのですが)
は、(2)のメソッドをそれぞれ名前を変えて複数用意することになります。
(一方、setUp()とtearDown()は一つずつしか存在しえません。)


そして実装が終わったテスト・ケースを実行すると、次の順番で実行されることに
なります。

(1) setUp()メソッド
(2) テスト・メソッド(testXXXX()メソッド)
(3) tearDown()メソッド

testXXXX()メソッドが複数ある場合は、それぞれのtestXXXX()メソッドごとに(1)〜(3)が
繰り返されることになります。


さてこれらのうち、setUp()メソッドとtestXXXX()メソッド(現時点の
HotelClientTestではtestReserveRoom()というメソッド名になっている)は
HotelClientTestのソース・コードの中に既に用意されていますね。これは、
前回(vol.160の最後のほうで)HotelClientTestクラスを生成する作業を行って
いたときに、「setUp()」と「reserveRoom()」にチェックマークを入れたために
用意されたものです。
そして「tearDown()」にはチェックマークを入れていなかったのでtearDown()メソッ
ドは用意されていません。


ところで、現時点でHotelClientTestのsetUp()メソッドの中を見ると、

super.setUp();

というふうにスーパークラス(すなわちTestCase)のsetUp()メソッドを呼び
出していますが、これはメソッドをオーバーライドするときの習慣として、
スーパークラスのメソッドを呼び出しておくようにしたもので、実際には
TestCaseのsetUp()メソッドは何もしません。したがって、この
super.setUp();
というコードはあってもなくても動作に変わりはありません。


では早速、テスト・ケースHotelClientTestを編集しましょう。
下記のように編集してみて下さい。

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

import java.rmi.RemoteException;
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();
      // ここでデータベースの準備を行う。(省略)
   }

   /**
    * 予約済みのデータでは予約できないことのテスト。
    */
   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");
      }
   }

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

さて、このsetUp()の中では、コメントにも書いたようにテストの準備として
データベースをテストにふさわしいデータに設定するためのコーディングを行っ
ておくべきですが、時間を節約するために(読者への課題とし)ここでは省略します。

ここでは、これまでにデータベースに書き込んだデータをそのまま利用してテスト
を行うことにします。
ただし、この場合、一回テストを行うとデータベースの中が書き換えられてしまうので、
もう一度同じテストを行うためにはデータベースを前の状態に手で復元してやる必要
があります。実際の開発の現場では、このようなテストは何度でも行うことになるため
毎回手で復元していては大変なので、setUp()の中でデータベースのデータをテストの
準備用に設定する(復元する)ためのコーディングを行っておくことが望ましいのです。

なお、テスト途中に他者(他のプログラム)からデータベースを書き換えられては
困りますから、テストに使用するデータベースは共有されないプライベートなもの
(このテスト専用のもの)とします。


さて、上のソース・コードでは、テスト・ケース(TestCaseクラス)専用のメソッド
が出てきましたので紹介しておきましょう。

まず、assertFalse()ですが、これは以前、契約による設計のお話で出てきたassert
とは関係はありません(念のため)。
このassertFalse()メソッドは、テストの結果を判定するためのメソッドです。
メソッド名からも分かるように、テスト対象の結果がfalseになることを主張(assert)
するもので、引数に指定したコードの実行結果がfalseだとテスト結果はOKだと判定し、
実行結果がtrueだとテスト結果は失敗だと判定します。

次に、fail()は、無条件にテストを失敗だと判定するメソッドです。ここではこのテスト
でRemoteExceptionやServiceExceptionが発生してはいけないので、これらの例外をcatch
したときに失敗と判定するようにしています。

assertTrue()というのはassertFalse()の逆のメソッドで、引数に指定したコードの
実行結果がtrueだとテスト結果はOKだと判定し、実行結果がfalseだとテスト結果は
失敗だと判定します。

JUnitでは、他にもassertで始まるメソッド名のメソッドがたくさん用意されています。
たとえば、
assertEquals(o1, o2)・・・第一引数と第二引数が同じ値であればテスト結果はOK
assertSame(o1, o2)・・・第一引数と第二引数が同一オブジェクトであればテスト結果はOK
assertNotSame(o1, o2)・・・第一引数と第二引数が同一オブジェクトでなければテスト結果はOK
assertNull(object)・・・引数の値がnullであればテスト結果はOK
assertNotNull(object)・・・引数の値がnullでなければテスト結果はOK
などがあります。
詳しくは、

http://www.junit.org/

のJavaDocの項などで調べてみて下さい。


上の各テスト・メソッドが何のテストをしているのかは、コメントを入れておいたので
分かると思いますが、このうちtestReserveRoom03()はちょっと特殊なので解説しておき
ましょう。
このtestReserveRoom03()は、宿泊日に"20101301"というように異常な日付(13月なんて
ありえない)を指定した場合に、HotelClient.reserveRoom(roomReserve)が
RemoteExceptionを返してくることを確認するためのテスト・メソッドです。
HotelClient.reserveRoom(roomReserve)の行でRemoteExceptionが発生するわけです
から、その真下の行は実行されずにそのままcatchブロックに進むはずです。
したがって、もし
fail("RemoteExceptionが発生しなければなりません。");
の行が実行されたら、テスト結果は失敗ということになります。そのためfail()メソッド
をそこに置いてあるわけです。
一方、ちゃんとRemoteExceptionが発生すればテスト結果はOKということになりますが、
このときはcatchブロックに進むことになるので、そこには
         // ここに来たときは、テスト結果はOKなので何もしない。
と書いてある通り、何もしていません。
fail()メソッドの逆はありませんので、代わりに何もしないでメソッドを終了すれば
テスト結果はOKと判定されるようになっています。


なお、上のソース・コードでは、テストの例として3つのメソッドしか用意していません
が、実際のテストでは、指定可能なデータの組み合わせがたくさんあるため、たくさん
のメソッドを用意することになります。実際にどのようなデータの組み合わせを計画し
てテスト・ケースを作ればいいかについては、後ほど詳しくお話しいたします。


さて、このテスト・ケースを実行すると、
(1) setUp()メソッド
(2) testReserveRoom01()メソッド
(3) (tearDown()メソッド)
(4) setUp()メソッド
(5) testReserveRoom02()メソッド
(6) (tearDown()メソッド)
(7) setUp()メソッド
(8) testReserveRoom03()メソッド
(9) (tearDown()メソッド)
という順番に実行されることになります。ただし、今回はtearDown()メソッドは
(用意していないので)実行されません。

ここで、ちょっと注意しておいて欲しいことは、この実行するテスト・メソッドごと
にテスト・ケース(HotelClientTest)のインスタンスが生成されるということ
です。
つまり、上の(1)〜(3)が実行されるときのHotelClientTestのインスタンスと、
(4)〜(6)が実行されるときのHotelClientTestのインスタンスと、(7)〜(9)が実行
されるときのHotelClientTestのインスタンスはそれぞれ別のものになります。

したがって、testReserveRoom01()メソッドを実行したときにHotelClientTestの
フィールドに設定したデータは、testReserveRoom02()メソッドやtestReserveRoom03()
メソッドを実行するときには反映されません。ただし、同じデータに設定するので
あれば、setUp()メソッドの中で設定を行っておけばテスト・メソッドの中で設定
せずに済みます。
上のソース・コードでも各テスト・メソッドの共通部分をsetUp()メソッドの中に
移せば、ソース・コードが短くなります。


では、実際にテストをしてみましょう。(予めTomcatを起動しておくことをお忘れなく。)

パッケージ・エクスプローラーの中でHotelClientTestを右クリックし、「実行」→
「JUnitテスト」を選択して下さい。

「パッケージ・エクスプローラー」タブの隣に「JUnit」タブが現れて、テスト結果が
表示されますね。緑色のマークはテスト結果OKを意味します。

では、試しに同じテストをもう一度実行して見て下さい。(「パッケージ・エクス
プローラー」タブをクリックしてから行って下さい。)

今度は、testReserveRoom02()に青いバッテンのマークが付きますね。これはテスト
が失敗したことを意味します。この青いバッテンが付いたtestReserveRoom02()を
クリックすると、その下の「障害トレース」という欄にエラーの箇所が示されます。
2行目の「at ...」という所に示されているのでその行をダブル・クリックしてみて
下さい。
HotelClientTestのソース・コードの中で該当箇所がハイライト(色が反転)されます。
assertTrue()の行ですね。ここが失敗しているということは、trueが返ってくるべき
ところがfalseが返ってきてしまったということになります。
今回は、データベースの前準備をせずに再度テストをしたのが悪いのであって、
プログラムの不具合ではありませんが、もしデータベースの前準備をきちんと行って
いる状態でこのような結果が出た場合は、ログなどで不具合の原因を追跡してプログラム
を修正し、再度テストを行う必要があります。

なお、「JUnit」タブ下に

実行: 3/3 エラー: 0 失敗: 1

という表示があるのは、3つのテスト・メソッドのうち3つが実行され、エラーは0個、
テスト結果が失敗だったものは1個であることを意味します。
ここで、エラー(赤いバッテンのマークが付いているもの)というのは、テスト・ケー
ス自体が例外を発生した場合を意味し、基本的にはテスト・ケース自体の問題になる
ので、テスト結果の失敗とは区別されます。



(次回に続く)


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



┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
★ホームページ:
      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. 不許無断複製