SeamFramework.orgCommunity Documentation

第37章 Seamアプリケーションのテスト

37.1. Seamコンポーネントのユニットテスト
37.2. Seamコンポーネントの統合テスト
37.2.1. モックを使用した統合テスト
37.3. ユーザーインタラクションの統合テスト
37.3.1. 設定
37.3.2. 別フレームワークでのSeamTestの利用
37.3.3. モックデータを利用した統合テスト
37.3.4. Seamメールの統合テスト

Seamアプリケーションのほとんどは、少なくとも2種類の自動テストが必要です。 個々のSeamコンポーネントを独立してテストするユニットテストと、 アプリケーションのすべてのJava層 (ビューページ以外のすべて) をスクリプトでテストする統合テストです。

どちらのテストもとても簡単に作成できます。

すべてのSeamコンポーネントはPOJOです。簡単にユニットテストを始めるにはとても良い環境です。さらにSeamは、コンポーネント間でのやり取りやコンテキスト依存オブジェクトへのアクセスにバイジェクションを多用しているので、通常のランタイム環境でなくてもとても簡単にSeamコンポーネントをテストすることができます。

次のような、顧客アカウントのステートメントを作成するSeamコンポーネントを考えてみましょう。

@Stateless

@Scope(EVENT)
@Name("statementOfAccount")
public class StatementOfAccount {
   
   @In(create=true) EntityManager entityManager
   
   private double statementTotal;
   
   @In
   private Customer customer;
   
   @Create
   public void create() {
      List<Invoice
> invoices = entityManager
         .createQuery("select invoice from Invoice invoice where invoice.customer = :customer")
         .setParameter("customer", customer)
         .getResultList();
      statementTotal = calculateTotal(invoices);
   }
   
   public double calculateTotal(List<Invoice
> invoices) {
      double total = 0.0;
      for (Invoice invoice: invoices)
      {
         double += invoice.getTotal();
      }
      return total;
   }
   
   // getter and setter for statementTotal
   
}

calculateTotalメソッドのユニットテスト(つまりこのコンポーネントのビジネスロジックのテスト)は、以下のように書くことができます。

public class StatementOfAccountTest {

    
    @Test
    public testCalculateTotal {
       List<Invoice
> invoices = generateTestInvoices(); // A test data generator
       double statementTotal = new StatementOfAccount().calculateTotal(invoices);
       assert statementTotal = 123.45;
    }   
}

データベースからデータを取り出したり保存したりするテストは行っていませんし、Seamが提供する機能のテストも行っていないことがおわかりいただけるでしょう。作成したPOJOのロジックをテストしているだけです。Seamコンポーネントは通常、コンテナのインフラストラクチャに直接依存していないため、ほとんどのユニットテストはこのように簡単に書くことができるのです!

アプリケーション全体をテストする場合は、以降を読み進んでください。

統合テストはもう少しだけ複雑になります。コンテナのインフラストラクチャはテスト対象の一部であるため、無視することができないのです!とはいえ、自動テストを実行するためにわざわざアプリケーションサーバーへアプリケーションをデプロイしたくはありません。そこで、最低限必要なコンテナのインフラストラクチャをテスト環境に再現し、性能を大きく損なうことなくすべてのアプリケーションを実行可能にする必要があります。

Seamが採用するアプローチは、コンポーネントのテストを作成し独立したコンテナ環境(SeamとJBoss内蔵のコンテナ:詳細は項30.6.1. 「Embedded JBoss をインストールする」参照)で実行するというものです。

public class RegisterTest extends SeamTest

{
   
   @Test
   public void testRegisterComponent() throws Exception
   {
            
      new ComponentTest() {
         protected void testComponents() throws Exception
         {
            setValue("#{user.username}", "1ovthafew");
            setValue("#{user.name}", "Gavin King");
            setValue("#{user.password}", "secret");
            assert invokeMethod("#{register.register}").equals("success");
            assert getValue("#{user.username}").equals("1ovthafew");
            assert getValue("#{user.name}").equals("Gavin King");
            assert getValue("#{user.password}").equals("secret");
         }
         
      }.run();
      
   }
   ...
   
}

難しいのはユーザーインタラクションをどのようにエミュレートするかです。そしてどこにアサーションを置くかです。あるテストフレームワークでは、すべてのアプリケーションをテストするのに、Webブラウザでユーザーのインタラクションを再生する必要があります。このようなフレームワークは存在意義はありますが、開発段階で使用するには適切ではありません。

SeamTestを使用して、擬似JSF環境でテストスクリプトを作成します。テストスクリプトの役割は、ビューとSeamコンポーネント間のインタラクションを再現することです。つまり、JSF実装のふりをするということです!

このアプローチではビューを除くすべてをテストすることができます。

さきほどユニットテストしたコンポーネントのJSFビューを考えてみましょう。


<html>
 <head>
  <title
>Register New User</title>
 </head>
 <body>
  <f:view>
   <h:form>
     <table border="0">
       <tr>
         <td
>Username</td>
         <td
><h:inputText value="#{user.username}"/></td>
       </tr>
       <tr>
         <td
>Real Name</td>
         <td
><h:inputText value="#{user.name}"/></td>
       </tr>
       <tr>
         <td
>Password</td>
         <td
><h:inputSecret value="#{user.password}"/></td>
       </tr>
     </table>
     <h:messages/>
     <h:commandButton type="submit" value="Register" action="#{register.register}"/>
   </h:form>
  </f:view>
 </body>
</html
>

このアプリケーションのユーザー登録機能(Registerボタンをクリックしたときの動作)をテストします。TestNG自動テストで、JSF要求のライフサイクルを再現してみましょう。

public class RegisterTest extends SeamTest

{
   
   @Test
   public void testRegister() throws Exception
   {
            
      new FacesRequest() {
         @Override
         protected void processValidations() throws Exception
         {
            validateValue("#{user.username}", "1ovthafew");
            validateValue("#{user.name}", "Gavin King");
            validateValue("#{user.password}", "secret");
            assert !isValidationFailure();
         }
         
         @Override
         protected void updateModelValues() throws Exception
         {
            setValue("#{user.username}", "1ovthafew");
            setValue("#{user.name}", "Gavin King");
            setValue("#{user.password}", "secret");
         }
         @Override
         protected void invokeApplication()
         {
            assert invokeMethod("#{register.register}").equals("success");
         }
         @Override
         protected void renderResponse()
         {
            assert getValue("#{user.username}").equals("1ovthafew");
            assert getValue("#{user.name}").equals("Gavin King");
            assert getValue("#{user.password}").equals("secret");
         }
         
      }.run();
      
   }
   ...
   
}

コンポーネントにSeam環境を提供するSeamTestを継承し、JSF要求のライフサイクルをエミュレートするSeamTest.FacesRequestを継承した無名クラスにテストスクリプトを記述していることに注目してください。(GET要求をテストするSeamTest.NonFacesRequestも用意されています。)さまざまなJSFフェーズを表す名前のメソッドに、JSFのコンポーネント呼び出しをエミュレートするコードを記述しています。さらに、さまざまなアサーションをスローしています。

Seamのサンプルアプリケーションには、もっと複雑なケースの統合テストが用意されています。Antを使用してテストを実行する方法と、EclipseのTestNGプラグインを使用する方法があります。

seam-genでプロジェクトを作成した場合は、すぐにテストを書き始めることができます。しかしそうでない場合は、お使いのビルドツール(ant, maven, Eclipseなど)のテスト環境を設定する必要があります。

まず、最低限必要な依存関係を見てみましょう。


エンベッドJBossが起動しなくなりますので、コンパイル時のJBoss AS依存ライブラリ(たとえばjboss-system.jar)をlib/からクラスパスに含めないでください。必要な依存ライブラリ(たとえばDroolsやjBPM)だけを追加してください。

エンベッドJBossの設定を含むbootstrap/ディレクトリもクラスパスに含めてください。

テストフレームワークのjarファイルはもちろん、プロジェクトとテストもクラスパスに含めてください。同じようにJPAとSeamのすべての設定ファイルもクラスパスに含めるのを忘れないでください。Seamでは、ルートにseam.propertiesを持つリソース(たとえばjarファイルやディレクトリ)はすべてエンベッドJBossにデプロイされます。すなわち、プロジェクトを含むデプロイ可能なアーカイブと類似したディレクトリ構造にしない場合は、それぞれのリソースにseam.propertiesを含めてください。

デフォルトでは、作成されたプロジェクトはjava:/DefaultDS(エンベッドJBossに組み込みのHSQLデータソース)をテストで使用します。別のデータソースを使用する場合は、foo-ds.xmlbootstrap/deployディレクトリに置いてください。

各テストの前にデータベースにデータを挿入したり、消去したりしたい場合はDBUnitと連携します。 SeamTest の替わりに DBUnitSeamTest を継承してください。

DBUnitのデータセットを記述しなければならりません。


<dataset>
   
   <ARTIST 
      id="1"
      dtype="Band"
      name="Pink Floyd" />
      
   <DISC
      id="1"
      name="Dark Side of the Moon"
      artist_id="1" />
      
</dataset
>

このテストクラスで、prepareDBUnitOperations()をオーバーライドしてデータセットを定義します。

protected void prepareDBUnitOperations() {

    beforeTestOperations.add(
       new DataSetOperation("my/datasets/BaseData.xml")
    );
 }

DataSetOperationはコンストラクタのもう一つの引数にオペレーションが指定されていないとDatabaseOperation.CLEAN_INSERTを仮定します。上記の例では各@Testメソッドが呼ばれる前にBaseData.xmlに定義されたすべてのテーブルのデータを消去し、次にBaseData.xmlに宣言されたすべての列を挿入します。

テストメソッドの実行後にさらにデータ消去が必要な場合はafterTestOperationsのリストにオペレーションを追加してください。

TestNGのテストパラメータdatasourceJndiNameにデータソース名を指定して、DBUnitにデータソースを知らせます。


<parameter name="datasourceJndiName" value="java:/seamdiscsDatasource"/>

DBUnitSeamTestはMySQLとHSQLをサポートします。どちらを使うか、以下のように設定してください。設定されなければ、HSQLがデフォルトです。


<parameter name="database" value="MYSQL" />

バイナリデータをテストデータセットに挿入することもできます(Windowsでは未検証ですので注意してください)。クラスパスにリソースの場所を以下のように指定してください。


<parameter name="binaryDir" value="images/" />

HSQLを使っていて、かつバイナリーのインポートが無いというのであれば、これらのパラメータは何も設定する必要はありません。しかし、テストの設定で datasourceJndiName を指定しない限り、テストを実行する前に setDatabaseJndiName() を呼び出さなければならなくなります。もしあなたが HSQL や MySQL を使わないなら、いくつかのメソッドをオーバーライドする必要があります。詳しくは、 DBUnitSeamTest の JavaDoc を参照してください。

Seamメールの統合テストはとても簡単です。

public class MailTest extends SeamTest {

    
   @Test
   public void testSimpleMessage() throws Exception {
        
      new FacesRequest() {
         @Override
         protected void updateModelValues() throws Exception {
            setValue("#{person.firstname}", "Pete");
            setValue("#{person.lastname}", "Muir");
            setValue("#{person.address}", "test@example.com");
         }
            
         @Override
         protected void invokeApplication() throws Exception {
            MimeMessage renderedMessage = getRenderedMailMessage("/simple.xhtml");
            assert renderedMessage.getAllRecipients().length == 1;
            InternetAddress to = (InternetAddress) renderedMessage.getAllRecipients()[0];
            assert to.getAddress().equals("test@example.com");
         }
            
      }.run();       
   }
}

いつも通りFacesRequestを生成します。invokeApplicationフックでは、viewIdにレンダリングするメッセージを指定しgetRenderedMailMessage(viewId);を呼び出し、メッセージをレンダリングします。メソッドはレンダリングされたメッセージを返しますので、メッセージに対してテストを行うことができます。もちろん標準JSFのどのライフサイクルメソッドも使用できます。

標準JSFコンポーネントのレンダリングはサポートしませんので、メールボディをテストするのは簡単ではありません。