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

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

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

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

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

次のようなSeamコンポーネントを考えてみましょう。

@Stateless
@Scope(EVENT)
@Name("register")
public class RegisterAction implements Register
{
   private User user;
   private EntityManager em;

   @In
   public void setUser(User user) {
       this.user = user;
   }
   
   @PersistenceContext
   public void setBookingDatabase(EntityManager em) {
       this.em = em;
   }
   
   public String register()
   {
      List existing = em.createQuery("select username from User where username=:username")
         .setParameter("username", user.getUsername())
         .getResultList();
      if (existing.size()==0)
      {
         em.persist(user);
         return "success";
      }
      else
      {
         return null;
      }
   }

}

このコンポーネントのTestNGテストは、以下のように書くことができます。

public class RegisterActionTest
{

    @Test
    public testRegisterAction()
    {
        EntityManager em = getEntityManagerFactory().createEntityManager();
        em.getTransaction().begin();
        
        User gavin = new User();
        gavin.setName("Gavin King");
        gavin.setUserName("1ovthafew");
        gavin.setPassword("secret");
        
        RegisterAction action = new RegisterAction();
        action.setUser(gavin);
        action.setBookingDatabase(em);
        
        assert "success".equals( action.register() );
        
        em.getTransaction().commit();
        em.close();
    }
    
    
    private EntityManagerFactory emf;
    
    public EntityManagerFactory getEntityManagerFactory()
    {
        return emf;
    }
    
    @Configuration(beforeTestClass=true)
    public void init() 
    {
        emf = Persistence.createEntityManagerFactory("myResourceLocalEntityManager");
    }
    
    @Configuration(afterTestClass=true)
    public void destroy()
    {
        emf.close();
    }
    
}

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

26.2. Seamアプリケーションの統合テスト

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

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

Seam が採用するアプローチは、コンポーネントのテスト・スクリプトを作成し、 独立したコンテナ環境 (Seam と JBoss 内蔵の EJB コンテナ) で実行すると言うものです。 テスト・スクリプトの基本的な役割は、 ビューと Seam コンポーネントの間のインタラクションを再現することです。 つまり、 JSF 実装のふりをするということです。

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

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

<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プラグインを使用する方法があります。

26.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 void processPayment(Payment payment) {
        return true;
    }
}

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