SeamFramework.org Community Documentation

- 前のページ
- 次のページ

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

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

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

## 37.1. Seamコンポーネントのユニットテスト

すべての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コンポーネントは通常、コンテナのインフラストラクチャに直接依存していないため、ほとんどのユニットテストはこのように簡単に書くことができるのです！

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

## 37.2. 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();

       }

       ...

    }

### 37.2.1. モックを使用した統合テスト

統合テスト環境では準備できないようなリソースをSeamコンポーネントが使用している場合、 コンポーネントの実装を置き換えることが必要な場合もあります。 たとえば支払処理システムのファサードを実装するSeamコンポーネントです。

    @Name("paymentProcessor")

    public class PaymentProcessor {

        public boolean processPayment(Payment payment) { .... }

    }

統合テストをするには、次のようなコンポーネントのモック実装を作成します。

    @Name("paymentProcessor")

    @Install(precedence=MOCK)

    public class MockPaymentProcessor extends PaymentProcessor {

        public boolean processPayment(Payment payment) {

            return true;

        }

    }

`MOCK`の優先度は、アプリケーションコンポーネントの デフォルト優先度より高いので、モック実装がクラスパスにあればSeamは モック実装を優先します。 本番環境ではモック実装は存在しないので、実際のコンポーネントが実行されます。

## 37.3. ユーザーインタラクションの統合テスト

難しいのはユーザーインタラクションをどのようにエミュレートするかです。そしてどこにアサーションを置くかです。あるテストフレームワークでは、すべてのアプリケーションをテストするのに、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プラグインを使用する方法があります。

### 37.3.1. 設定

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

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

表 37.1.

グループID | アーティファクトID | Seam での場所
--- | --- | ---
`org.jboss.seam.embedded` | `hibernate-all` | `lib/test/hibernate-all.jar`
`org.jboss.seam.embedded` | `jboss-embedded-all` | `lib/test/jboss-embedded-all.jar`
`org.jboss.seam.embedded` | `thirdparty-all` | `lib/test/thirdparty-all.jar`
`org.jboss.seam.embedded` | `jboss-embedded-api` | `lib/jboss-embedded-api.jar`
`org.jboss.seam` | `jboss-seam` | `lib/jboss-seam.jar`
`org.jboss.el` | `jboss-el` | `lib/jboss-el.jar`
`javax.faces` | `jsf-api` | `lib/jsf-api.jar`
`javax.el` | `el-api` | `lib/el-api.jar`
`javax.activation` | `javax.activation` | `lib/activation.jar`

エンベッド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.xml`を`bootstrap/deploy`ディレクトリに置いてください。

### 37.3.2. 別フレームワークでのSeamTestの利用

SeamではTestNGであればすぐに使用できますが、JUnitなどの別のテストフレームワークを利用することもできます。

以下の要領で`AbstractSeamTest`を実装してください。

- すべてのテストメソッドの前に`super.begin()`を呼ぶ。

- すべてのテストメソッドの後に`super.end()`を呼ぶ。

- 統合テスト環境をセットアップする`super.setupClass()`を呼ぶ。テストメソッドのどれかが呼ばれる前に呼ばれるようにしてください。

- 統合テスト環境を消去する`super.cleanupClass()`を呼ぶ。

- 統合テストの開始時に`super.startSeam()`を呼び、Seamを起動する。

- 統合テストの終了時に`super.stopSeam()`を呼び、Seamを停止する。

### 37.3.3. モックデータを利用した統合テスト

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

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

## 注意

DBUnitは平文とXMLの2種類の形式のデータセットファイルをサポートします。Seamの `DBUnitSeamTest` は平文フォーマットを前提としていますのでこちらの形式を使用してください。

    <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 を参照してください。

### 37.3.4. Seamメールの統合テスト

## 注意

警告！この機能はまだ開発中です。

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コンポーネントのレンダリングはサポートしませんので、メールボディをテストするのは簡単ではありません。

- 前のページ第36章 パフォーマンスチューニング
- 上に戻る
- ホーム
- 次のページ第38章 Seam ツール