DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(2)

前回のブログに引き続きDeltaSpike Dataを使ったJava EEアプリケーションをArquillianを使ってテストする方法について説明します。DeltaSpike Data自身はJava EEサーバーに依存していませんが、今回のサンプルプログラムはJPAのpersistence.xmlがデータベースにアクセスするのにWildFlyのデータソースを使っているのでテストを実行するにはWildFlyが必要になります。このようなアプリサーバーに依存するテストはArqullianを使うことで可能になります。

Arquillianとは

Java EE アプリケーションは、REST/CDI/EJB/JMSなどサーバー上のコンテナに依存することが多いのでJUnitを使ったテストの自動化は簡単ではありません。

ArquillianはJava EEアプリケーションのためのテスティング・フレームワークです。アプリケーションをJava EEサーバー上で動作させてテストをしたいとき、以下のような処理をしたいことがあります。

  • サーバーを起動する
  • アプリケーションをサーバーにデプロイする
  • サーバー上でテストを実行する
  • アプリケーションをサーバーからアンデプロイする
  • サーバーを停止する

Arquillianは、これらサーバーの起動・停止やデプロイ・アンデプロイを代わりにやってくれます。テストの実行は使い慣れたJUnit(やTestNG)を使います。しかも、JUNitのテストプログラムはサーバー上で実行され、CDIやEJBなどのBeanはテスト内にインジェクトして参照することができます。これにより、通常のJUnitを使ったテストと同様にテストをアプリサーバー上で実行することができます。

Arquillianテストプログラムの構造

Arquillianのテストプログラムは以下のような構造になります。

@RunWith(Arquillian.class) // (1)
public class MemberRegistrationTest {
    @Deployment // (2)
    public static Archive<?> createTestArchive() {
    ...
    }
    @Test // (3)
    public void testRegister() throws Exception {
    ...
    }
}
  • (1) @RunWith()でTestRunnerとしてArquillian.classを指定します。
  • (2) @DeploymentでアプリサーバーにデプロイするモジュールをShrinkWrap APIで作成します。
  • (3) @Testでテストメソッドを指定します。

この@Deploymentの部分がArquillian固有の特徴のある部分です。Arquillianではアプリサーバー上でテストプログラムを実行するためのアーカイブをShrinkWrapというAPIを使ってプログラムで作成します。これはメモリ上でアーカイブを作る操作になります。このアーカイブにはアプリケーションのすべてのクラスを含めるのではなく、テストに必要なクラスだけ含めるようにします。このようにアーカイブを作ることでデプロイするアーカイブのサイズを小さくできますし、テストの用途に応じて、クラスやデプロイメントディスクリプタを差し替えることも可能です。

Arquillianのテストプログラム

前回のブログで作成したアプリケーションは、Mavenアーキタイプによって生成しましたが、それにはすでにArquillianに対応したテストプログラムが付いていました。それをベースにDeltaSpike Data用に修正します。修正のポイントは以下の通り。

  • DataSpike Dataの依存ライブラリを追加する
  • アーカイブにDeltaSpike Dataを使ったMemberRepository.classを追加する
  • アーカイブにMETA-INF/apache-deltaspike.propertiesを追加する

注意:以下のテストプログラムでは直接MemberRepositoryを参照していませんが、MemberRegistrationのクラスにおいてそれをインジェクトしているために、アーカイブに含めておく必要があります。以下のサンプルではpom.xmlを参照して必要な依存ライブラリを抽出しています。

@RunWith(Arquillian.class)
public class MemberRegistrationTest {
    @Deployment
    public static Archive<?> createTestArchive() {
    	// DeltaSpike Dataの依存ライブラリをアーカイブに追加する
    	File[] files = Maven.resolver()
                .loadPomFromFile("pom.xml")
                //.importRuntimeDependencies().resolve()
                .resolve("org.apache.deltaspike.modules:deltaspike-data-module-api", "org.apache.deltaspike.modules:deltaspike-data-module-impl")
                .withTransitivity()
                .asFile();
    	
    	// アーカイブにテストプログラムが参照するクラスを追加する
    	Archive<?> archive = ShrinkWrap.create(WebArchive.class, "test.war")
                .addClasses(Member.class, MemberRepository.class, MemberRegistration.class, Resources.class)
                .addAsLibraries(files)
                .addAsResource("META-INF/test-persistence.xml", "META-INF/persistence.xml")
                .addAsResource("META-INF/test-apache-deltaspike.properties", "META-INF/apache-deltaspike.properties")
                .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
                // Deploy our test datasource
                .addAsWebInfResource("test-ds.xml");
    	
    	// デバッグ用にアーカイブの内容を標準出力に出力する
        System.out.println("archive=" + archive.toString(true));

        return archive;
    }

    @Inject
    MemberRegistration memberRegistration;

    @Inject
    Logger log;

    @Test
    public void testRegister() throws Exception {
	    Member newMember = new Member();
	    newMember.setName("Jane Doe");
	    newMember.setEmail("jane@mailinator.com");
	    newMember.setPhoneNumber("2125551234");
	    memberRegistration.register(newMember);
	    assertNotNull(newMember.getId());
	    log.info(newMember.getName() + " was persisted with id " + newMember.getId());  
    }
}

Arquillianの実行

Arquillianは内部でアプリサーバーと通信してテストケースを実行しますので、テストを実行するにはアプリサーバーを実行する必要があります。アプリサーバーの利用方法としては、remote/managed/embeddedの3種類が可能です。今回はテスト実行時にアプリサーバーの起動・停止をおこなうmanagedモードを選択します。

managedでWildFlyと通信可能にするため環境変数JBOSS_HOMEを設定してください(または、arquillian.xmlにWildFlyのパスを書くことで環境変数を設定しないことも可能です)。

テストの実行方法はmavenコマンドのプロファイルとしてarq-wildfly-managedを指定します。

mvn clean test -Parq-wildfly-managed

このテストを実行すると自動的にWildFlyが起動され、テストプログラムがWildFly上で実行されます。このプログラムではデバッグ用にアーカイブを標準出力に出していますが、その結果が以下にようになっています。DeltaSpike Dataが内部で使っているDeltaSpike Coreなどのjarがいくつも追加されている点に注意してください。

$ mvn clean test -Parq-wildfly-managed
[INFO] Scanning for projects…
[INFO]
[INFO] ————————————————————————
[INFO] Building WildFly Quickstarts: tanoseam-webapp 1.0.0-SNAPSHOT
[INFO] ————————————————————————
[INFO]
// (略)
00:12:57,148 INFO [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: WildFly Full 10.0.0.CR4 (WildFly Core 2.0.0.CR8) started in 8679ms – Started 387 of 609 services (326 services are lazy, passive or on-demand)
archive=test.war:
/WEB-INF/
/WEB-INF/test-ds.xml
/WEB-INF/lib/
/WEB-INF/lib/deltaspike-partial-bean-module-impl-1.5.2.jar
/WEB-INF/lib/deltaspike-data-module-api-1.5.2.jar
/WEB-INF/lib/deltaspike-data-module-impl-1.5.2.jar
/WEB-INF/lib/deltaspike-core-impl-1.5.2.jar
/WEB-INF/lib/deltaspike-jpa-module-impl-1.5.2.jar
/WEB-INF/lib/deltaspike-proxy-module-api-1.5.2.jar
/WEB-INF/lib/deltaspike-proxy-module-impl-asm5-1.5.2.jar
/WEB-INF/lib/deltaspike-jpa-module-api-1.5.2.jar
/WEB-INF/lib/deltaspike-partial-bean-module-api-1.5.2.jar
/WEB-INF/lib/deltaspike-core-api-1.5.2.jar
/WEB-INF/classes/
/WEB-INF/classes/META-INF/
/WEB-INF/classes/META-INF/apache-deltaspike.properties
/WEB-INF/classes/META-INF/persistence.xml
/WEB-INF/classes/org/
/WEB-INF/classes/org/tanoseam/
/WEB-INF/classes/org/tanoseam/examples/
/WEB-INF/classes/org/tanoseam/examples/data/
/WEB-INF/classes/org/tanoseam/examples/data/MemberRepository.class
/WEB-INF/classes/org/tanoseam/examples/util/
/WEB-INF/classes/org/tanoseam/examples/util/Resources.class
/WEB-INF/classes/org/tanoseam/examples/service/
/WEB-INF/classes/org/tanoseam/examples/service/MemberRegistration.class
/WEB-INF/classes/org/tanoseam/examples/model/
/WEB-INF/classes/org/tanoseam/examples/model/Member.class
/WEB-INF/beans.xml// (略)

0:13:01,164 INFO [org.jboss.as] (MSC service thread 1-5) WFLYSRV0050: WildFly Full 10.0.0.CR4 (WildFly Core 2.0.0.CR8) stopped in 83ms

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ————————————————————————
[INFO] BUILD SUCCESS
[INFO] ————————————————————————
[INFO] Total time: 17.805 s
[INFO] Finished at: 2016-01-17T00:13:01+09:00
[INFO] Final Memory: 36M/395M
[INFO] ————————————————————————

DeltaSpike DataをJava EEアプリケーションサーバー上で動かす(1)

前回のブログで紹介したDeltaSpike DataモジュールをJava EEアプリケーションサーバー上で動かしてみましょう。どのアプリケーションサーバーを使ってもよいのですが、このブログではWildFlyを使った手順を紹介します。WildFlyにはCDIコンテナとしてWeldが組み込まれていますし、H2データベースが最初からインストールされているので、DeltaSpike Dataモジュールをすぐに試してみることができます。このページの最後にサンプルプログラムのリンクをつけていますので実際にプログラムを動かしながら確認してください。

WildFlyのインストール

WildFlyはJBoss.orgにおいてオープンソースで開発されているJava EE 7に対応したアプリケーションサーバーです(以前はJBossプロジェクトのアプリケーションサーバーはJBoss ASと呼ばれていましたが、JBoss ASはVersion 7までで、Version 8からWildFlyはという名前に変更されました)。

WildFlyの最新安定バージョンは9.0.1.Finalですが、今回は10.0.0.CR4を使うことにします。以下のサイトからwildfly-10.0.0.CR4.zipをダウンロードして、適当なディレクトリで解凍してください。

WildFlyの起動と停止

WildFlyの起動方法にはStandaloneモードとDomainモードの二つがあります。ここでは、単一サーバーで確認するためStandaloneモードで起動します。

$ cd wildfly-10.0.0.CR4/bin
$ ./standalone.sh

 

以後、WildFlyを起動したままの状態で作業を進めますが、サーバーの停止の際は起動したコンソール上でCntrl-Cを押してください。

ベースとなるアプリケーションの準備

DeltaSpike Dataモジュールのサンプルはスクラッチから作るのではなくて、ありもののJava EEアプリケーションを修正して作ることにします。WildFly用のJava EE 7 Webアプリケーションを作成するMavenアーキタイプを使うことにしましょう。以下のコマンドを叩くと、tanoseam-webappという名前のMavenプロジェクトが作成されます。

$ mvn archetype:generate -DarchetypeGroupId=org.wildfly.archetype -DarchetypeArtifactId=wildfly-javaee7-webapp-archetype -DarchetypeVersion=8.2.0.Final -DgroupId=org.tanoseam.examples -DartifactId=tanoseam-webapp -Dversion=1.0.0-SNAPSHOT -DinteractiveMode=false

 

このアプリケーションはkitchensinkと呼ばれるJSF/REST/EJB/JPAを使ったWebアプリケーションです。このJPAの部分をDeltaSpike Dataモジュールを使って置き換えます。

DeltaSpikeに対応させるためのアプリケーションの修正

アプリケーション内で直接EntityManagerを操作している箇所をDeltaSpike Dataを使うように修正します。このアプリケーションでは、メンバーの検索はMemberRepository、メンバー登録はMemberRegistrationというクラスが担当していますので、それぞれ修正が必要です。

1. アプリケーションのpom.xmlにDeltaSpike Dataの依存ライブラリを追加します。バージョンは最新版の1.5.2を指定しましょう。

		<dependency>
			<groupId>org.apache.deltaspike.modules</groupId>
			<artifactId>deltaspike-data-module-api</artifactId>
			<version>${version.deltaspike}</version>
		</dependency>

		<dependency>
			<groupId>org.apache.deltaspike.modules</groupId>
			<artifactId>deltaspike-data-module-impl</artifactId>
			<version>${version.deltaspike}</version>
		</dependency>

2. tanoseam-webappプロジェクトのソースを修正して、データベースの検索部分をDeltaSpike Dataを使うように変更します。修正箇所は3つのファイルです。

  • org.tanoseam.examples.data.data.MemberRepository.javaを(前回のブログで紹介した)DeltaSpike Dataモジュールを使ったリポジトリ実装で置き換えてください。
package org.tanoseam.examples.data;

import org.tanoseam.examples.model.Member;
import org.apache.deltaspike.data.api.EntityRepository;
import org.apache.deltaspike.data.api.Query;
import org.apache.deltaspike.data.api.Repository;
import java.util.List;

@Repository
public interface MemberRepository extends EntityRepository<Member, Long> {

    @Query("select m from Member m where m.email = ?1")
	public Member findByEmail(String email);

    @Query("select m from Member m order by name")
	public List<Member> findAllOrderedByName();
}
  • 次に、RESTサービスを上記リポジトリが提供するインタフェースに合わせて修正します。
    org.tanoseam.examples.rest.MemberResourceRESTService.javaのlookupMemberById()メソッドにおいてfindById(id)をfindBy(id)へ変更してください。
@GET
@Path("/{id:[0-9][0-9]*}")
@Produces(MediaType.APPLICATION_JSON)
public Member lookupMemberById(@PathParam("id") long id) {
    //Member member = repository.findById(id);
    Member member = repository.findBy(id);
    if (member == null) {
        throw new WebApplicationException(Response.Status.NOT_FOUND);
    }
    return member;
}
  • 最後に、org.tanoseam.examples.service.MemberRegistration.javaを修正します。MemberRepositoryのBeanを変数repositoryにインジェクトして、em.persist(member)をrepository.save(member)に置き換えてください。
@Inject
private MemberRepository repository;

public void register(Member member) throws Exception {
    log.info("Registering " + member.getName());
    //em.persist(member);
    repository.save(member);
    memberEventSrc.fire(member);
}

WildFlyへのデプロイ

ソースを修正したtanoseam-webappプロジェクトをビルドし、WildFlyへデプロイします。WildFlyへのデプロイには管理コンソールを使うことも可能ですが、WildFlyへのデプロイ用のMavenプラグインがありますので、今回はそれを使います。

$ cd tanoseam-webapp
$ mvn clean package wildfly:deploy

 

アプリケーションの実行

デプロイしたアプリケーションを実行するにはWebブラウザで以下を指定します。

このアプリケーションはメンバー情報(Name, Emai, Phone#)を入力する画面を表示します。各入力項目を入力してRegisterボタンを押下するとメンバーが登録されるはずです。しかし、今回ソースを修正したものを実行すると下図のようにボタンの右側に”A JTA EntityManager cannot use getTransaction()”というエラーメッセージが表示されてしまいます。

tanoseam-webapp

WildFlyのコンソールログを見ると、DeltaSpike Dataモジュールの処理の途中でIllegalStateException例外がスローされてEJB呼び出しが失敗しているようです。スタックトレースをよく見ると、この例外はResourceLocalTransactionStrategyというクラスのgetTransaction()メソッドで発生しています。

Caused by: java.lang.IllegalStateException: A JTA EntityManager cannot use getTransaction()
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.getTransaction(AbstractEntityManagerImpl.java:1333)
at org.jboss.as.jpa.container.AbstractEntityManager.getTransaction(AbstractEntityManager.java:518)
at org.apache.deltaspike.jpa.impl.transaction.ResourceLocalTransactionStrategy.getTransaction(ResourceLocalTransactionStrategy.java:370)
at org.apache.deltaspike.jpa.impl.transaction.ResourceLocalTransactionStrategy.rollbackAllTransactions(ResourceLocalTransactionStrategy.java:336)
at org.apache.deltaspike.jpa.impl.transaction.ResourceLocalTransactionStrategy.execute(ResourceLocalTransactionStrategy.java:154)
at org.apache.deltaspike.data.impl.tx.TransactionalQueryRunner.executeTransactional(TransactionalQueryRunner.java:72)
at org.apache.deltaspike.data.impl.tx.TransactionalQueryRunner.executeQuery(TransactionalQueryRunner.java:54)
at org.apache.deltaspike.data.impl.handler.QueryHandler.process(QueryHandler.java:122)
… 127 more

 

実行時エラーの回避

DeltaSpike DataはDeltaSpike JPAモジュールを使ってトランザクションを制御しています。このエラーは、DeltaSpike JPAモジュールにおけるデフォルトのトランザクションタイプがRESOURCE_LOCALであることが原因です。JPAではRESOURCE_LOCALなトランザクションの場合はEntityManager APIを使ってEntityTransactionを取得し、明示的にトランザクションを開始する必要があります。

しかし、今回のサンプルではMemberRegistryというEJB内部でDeltaSpike Dataのリポジトリ呼び出していますので、リポジトリの処理を開始する前にEJBによってトランザクションはすでに開始されています。そこで、DeltaSpike JPAモジュールはRESOURCE_LOCALなトランザクションを開始することはできず、今回のエラーに至ったと考えられます。

このエラーを回避するには、DeltaSpike Dataにおけるトランザクションの振る舞いをCMT(Container Managed Transaction)に対応させるように変更する必要があります。これを行うには、META-INF/apache-deltaspike.propertiesに以下のようにTransactionStrategyとしてContainerManagedTransactionStrategyを設定します。

globalAlternatives.org.apache.deltaspike.jpa.spi.transaction.TransactionStrategy=org.apache.deltaspike.jpa.impl.transaction.ContainerManagedTransactionStrategy

 

動作確認

エラーを回避する設定をした後にアプリケーションを再デプロイして、いくつかメンバーを登録してみましょう。表のメンバーの表示はMemberListProducerによって行なわれ、その中でMemberRepositoryのfindAllOrderedByName()メソッドが呼び出されています。表のメンバーが名前の順(order by name)になっていることを確認してください。下図の例では(idから類推できるように)David, Bob, Andyの順に登録しましたが、表示上は名前でソートされてAndy, Bob, Davidの順になっています。

members

注意:今回のプログラムではH2データベースを使っていますが、これはデモやテスト用途に適したインメモリの設定になっているため、アプリケーションの再デプロイやサーバーの再起動によってDBに登録済みのデータは消えてしまいます。本番環境でDBを利用する場合は、H2の設定を変更するか、PostgreSQLやMySQLなどの他のデータベースを検討してみてください。

サンプルプログラム

この修正済みのプログラムはこちらからダウンロードできます。

少し手間がかかりましたが、WildFlyという実行環境が準備できるとDeltaSpike Dataのさまざまな機能を実機で試すことができるようになります。次回は、Arquillianを使ってDeltaSpike Dataの機能をテストする方法を説明します。

DeltaSpike Dataの紹介

DeltaSpike Dataモジュールは、Repositoryパターンを実現したモジュールです。JPAを使ったプログラミングでは同様なコードを何度も書く必要があります。Dataモジュールを使うとデータベースへのCRUD関連の機能やクエリ定義をリポジトリインタフェースに集約することができます。さらに、DTO (Data Transfer Object)とエンティティの間のマッピングを提供するなど、なかなか面白い機能も提供しています。

Dataモジュールを使ってリポジトリを作るには次のようなインタフェースを用意します。

例えば、Memberというエンティティクラスがあって、主キーの型がLongの場合、次のようにしてリポジトリを定義します。

@Repository
public interface MemberRepository extends EntityRepository<Member, Long> {}

たったこれだけで、JPAのAPIを使うことなく、Entityに簡単にアクセスできるようになります。EntityRepositoryインタフェースは、以下のようなメソッドを提供しています(雰囲気を伝えるため代表的なものをピックアップしています)。

  • E save(E entity)
  • void remove(E entity)
  • void refresh(E entity)
  • void flush()
  • E findBy(PK pk)
  • List<E> findAll()
  • Long count();

DeltaSpike Dataの簡単な使い方

リポジトリは@Injectで変数にインジェクトして使います。インジェクトのときに指定する型は@Repositoryをつけたインタフェースです。例えば、エンティティを保存するには次のようにします。

@Inject
private MemberRepository repository;

Member member = new Member();
member.setName("Neverbird");
member.setEmail("neverbird@mail.com");
member.setPhoneNumber("1234567890");

repository.save(member);

実装クラスがなくともリポジトリを利用できる理由

さて、ここまででリポジトリの実装クラスが登場していないのに気付きましたか? DeltaSpikeを使うと、インタフェースを宣言するだけでリポジトリを利用できるのです。実は、これは前回のブログ記事で紹介したPartialBeanモジュールを使っています。@Repositoryアノテーションのソースを確認すると、確かに@PartialBeanBindingを使って定義されています。

@Stereotype
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@PartialBeanBinding
public @interface Repository
{
 Class forEntity() default Object.class;
 String methodPrefix() default "";
}

PartialBeanは、インタフェースへのアクセスからInvocationHandlerへの呼び出しを行う仕組みを提供します。DeltaSpike DataモジュールはEntityRepositoryインタフェースのJPAによる実装をInvocationHandlerとして提供しています。@Repositoryを使ってリポジトリのインタフェースを宣言することで、リポジトリインタフェースへの呼び出しが内部でInvocationHandlerの呼び出しに変換されているのです。

セットアップ

DeltaSpike Dataモジュールを使うためにはpom.xmlに以下の依存ライブラリを追加します。

<dependency>
 <groupId>org.apache.deltaspike.modules</groupId>
 <artifactId>deltaspike-data-module-api</artifactId>
 <version>${version.deltaspike}</version>
</dependency>

<dependency>
 <groupId>org.apache.deltaspike.modules</groupId>
 <artifactId>deltaspike-data-module-impl</artifactId>
 <version>${version.deltaspike}</version>
</dependency>

クエリの定義

EntityRepositoryインタフェースにはfindByとfindAllしか検索方法が提供されていません。これでは実際のアプリケーションでは使い物にならないのでEntity定義に合わせて必要な検索のためのメソッドをリポジトリに追加します。

@Repository
public interface MemberRepository extends EntityRepository {

 @Query("select m from Member m where m.email = ?1")
 public Member findByEmail(String email);
}

DeltaSpike Dataモジュールの最大の特徴はクエリの定義方法です。上の例は検索メソッドに@Queryというアノテーションを付加することでクエリを定義しています。@Queryで指定しているのはJPAでサポートしているJPQLの文字列です。findByEmailメソッドの例のように検索メソッドに引数を指定することもできます。一見してわかるように、この書き方を使うとJPQLの式と検索メソッドの結びつきを直感的に理解することができます。

リポジトリのユーザーは以下のようにリポジトリに定義されたクエリのメソッドを呼び出します。

@Inject
private MemberRepository repository;

Member member = repository.findByEmail("neverbird@email.com")

DeltaSpike Dataの最大の特徴はクエリ定義だと思います。Dataモジュールでは@Query以外にも豊富なクエリの定義方法を提供しています。次回は、WildFly上で実際にDeltaSpike Dataを動かしながら、その機能を確認していきましょう。

PartialBeanによるタイプセーフなハンドラー実装

DeltaSpike PartialBeanモジュールはインタフェース(または、抽象クラス)とハンドラーをバインドする仕組みを提供します。PartialBeanを使うとBeanの実装クラスを(Reflectionでお馴染みの)InvocationHandlerとして実装することができます。Partial(部分的な、不完全な)というのは、インタフェースを実装すればハンドラーが実装を提供するので、開発者がBeanの完全な実装をしなくとも良いという意味でしょう。

InvocationHandlerというと、それを聞いた人はきっとJavaのDynamic Proxyを想像されると思います。Dynamic ProxyではProxyへの呼び出しはターゲットのオブジェクトを呼び出します。しかし、このPartialBeanは実装上の仕組みとしてはDynamic Proxyを使ってはいるものの、ターゲットのBeanが別に存在するのではなく、ハンドラー自身がCDI Beanとして扱われます。ここが重要です。PartialBeanはCDIのインタセプターとは異なり、Beanの呼び出しに割り込んで何かを処理するものではなく、Beanそのものとしてインジェクションの対象になるのです。

そこで、PartialBeanではインタフェース部分の定義が重要になります。PartialBeanでは、インタフェースにおけるメソッド定義やメソッドに付加されたアノテーションの情報に基づいてハンドラーが処理を実行します。例えば、DeltaSpike DataモジュールではQueryの定義の実装にPartialBeanを使用しています。クエリの式をメソッドのアノテーションとして提供するのです。

@Query("select p from Person p where p.ssn = ?1")
Person findBySSN(String ssn);

これが意味することをよく考えてみましょう。ハンドラーはアノテーションの情報を使ってクエリを実行することができます。開発者はインタフェースからSQL式を理解することができ、しかもクエリを実行するときはメソッドを実行すれば良いのです。メソッドは引数の型や戻り値の型を提供しますから、呼び出し側が間違った呼び出しをしてもコンパイル時に型の不整合を検出することができます。つまり、タイプセーフです。PartialBeanは文字列の世界とタイプセーフな世界をつなぐ面白い仕組みと言えるでしょう。

PartialBeanのセットアップ

PartialBeanモジュールを使うにはMaven pom.xmlの依存ライブラリとして以下の設定を追加します。

		<dependency>
		    <groupId>org.apache.deltaspike.modules</groupId>
		    <artifactId>deltaspike-partial-bean-module-api</artifactId>
		    <version>${deltaspike.version}</version>
		    <scope>compile</scope>
		</dependency>
		
		<dependency>
		    <groupId>org.apache.deltaspike.modules</groupId>
		    <artifactId>deltaspike-partial-bean-module-impl</artifactId>
		    <version>${deltaspike.version}</version>
		    <scope>runtime</scope>
		</dependency>

@PartialBeanBindingの定義

インタフェースとハンドラーを結びつけるためには@PartialBeanBinding使って定義されたアノテーションが必要です。この例では@MyPartialBeanBindingというアノテーションを定義しています。

package org.tanoseam.examples;

import org.apache.deltaspike.partialbean.api.PartialBeanBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@PartialBeanBinding
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyPartialBeanBinding {}

インタフェース実装の例

次はPartialBeanのインタフェースの例です。@MyPartialBeanBindingというアノテーションをつけているので、これがPartialBeanであることがわかります。また、メソッド定義に@ValueHolderというアノテーションが付いているのに注意してください。このアノテーションは、後で、ハンドラー実装で参照します。

package org.tanoseam.examples;

import javax.enterprise.context.Dependent;

@Dependent
@MyPartialBeanBinding
public interface MyPartialBean {
    @ValueHolder(value="PartialBean")
    String getValue();
}

ハンドラーの実装

PartialBeanのハンドラーはInvocationHandlerのInvokeメソッドを実装します。
このサンプルプログラムでは、メソッドに付加されたアノテーションからvalueを取得しています。

package org.tanoseam.examples;

import java.lang.reflect.Method;
import javax.enterprise.context.Dependent;

@Dependent
@MyPartialBeanBinding
public class MyPartialBeanHandler implements java.lang.reflect.InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        ValueHolder holder = method.getAnnotation(ValueHolder.class);
        String value = holder.value();
        return value;
    }
}

テストコード

PartialBeanの使用方法は簡単で、通常のBeanと同様に@Injectで変数に設定することができます。

package org.tanoseam.examples;

import static org.junit.Assert.assertEquals;

import org.apache.deltaspike.testcontrol.api.junit.CdiTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Inject;

@RunWith(CdiTestRunner.class)
public class PartialBeanTest {

    @Inject
    private MyPartialBean bean;

    @Test
    public void testPartialBean() {
          assertEquals("PartialBean", bean.getValue());
    }
}

単体テストのためのCDIコンテキスト制御

これまでCDIを使った単体テストについて何回か紹介しましたが、そこでは主にインジェクションの対象を切り替える方法について書いてきました。しかし、CDI(Contexts and Dependency Injection)のインジェクションは、単なるインジェクションではなく、インジェクトされるBeanはコンテキストが管理しているのが特徴です。CDIでは、コンテキストの寿命が尽きれば、それにバインドしたBeanも解放されます。そこで、テストケースとしてコンテキストの開始・停止をすることでBeanのライフサイクルを制御をしたいことがあると思います。今回は、DeltaSpikeのCDITestRunnerを使ったときのコンテキストの制御について紹介します。

ContextControl

CDITestRunnerを使ったテストコードでは、ContextControlを使ってApplication/Session/Requestのスコープを制御することができます。

@Inject
private ContextControl contextControl;

ContextControlには以下の機能があります。

  • startContexts()    すべてのコンテキストの開始
  • stopContexts()    すべてのコンテキストの停止
  • startContext(Class<? extends Annotation> scopeClass)  指定されたスコープのコンテキストの開始
  • stopContext(Class<? extends Annotation> scopeClass)  指定されたスコープのコンテキストの停止

ContextControlのサンプルコード

次のサンプルコードは、ApplicationScopeのLoggerとSessionScopeのShoppingCartを題材としてContextControlによるコンテキスト制御をした例になります。

@RunWith(CdiTestRunner.class)
public class ContainerControlTest {
	
	@Inject 
	private Logger logger; // ApplicationScoped

	@Inject
	private ShoppingCart shoppingCart; // SessionScoped

	@Inject
	private ContextControl contextControl;
	
	@Test
	public void testScope() {	
		shoppingCart.put(new Product("001", 1));
		shoppingCart.put(new Product("002", 3));
		shoppingCart.put(new Product("003", 7));
		logger.info("ShoppingCart=>" + shoppingCart);
		contextControl.stopContexts();
		contextControl.startContexts();
		logger.info("ShoppingCart=>" + shoppingCart);
		assertTrue(shoppingCart.isEmpty());
	}
}

CdiTestRunnerでは、テストメソッドtestScope()が呼び出される前に、すべてのコンテキストは自動的に開始されます。この例では、 すべてのコンテキストを明示的に停止するためにcontextControl.stopContexts()を呼び出します。これにより、 ApplicationScopeのLoggerとSessionScopeのShoppingCartはコンテキストから解放されます。次に、続いて contextControl.startContexts()を呼び出すことで、すべてのコンテキストが開始されます。この後ShoppingCart を参照すると、新しいコンテキストにバインドするために新規に生成されますので、その結果として、カートに登録したプロダクトは空に初期化されます。

以下はテスト実行時に出力されたコンソールログの抜粋です。Loggerは@PostConstructでLogger::initialize、@PreDestroyのメソッドでLogger::destroyをコンソールに出力しています。ShoppingCartについても同様にコンソール出力します。コンソール出力を見ると、contextControl.stopContexts()によってShoppingCartとLoggerの両方がdestroyされていることが確認できます。

11 24, 2015 11:35:28 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted
情報: [run] org.tanoseam.examples.ContainerControlTest#testScope
Logger::initialize
ShoppingCart::initialize
ShoppingCart=>ShoppingCart [products=[Product [productId=001, quantity=1], Product [productId=002, quantity=3], Product [productId=003, quantity=7]]]
ShoppingCart::destroy
Logger::destroy
Logger::initialize
ShoppingCart::initialize
ShoppingCart=>ShoppingCart [products=[]]
11 24, 2015 11:35:28 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished
情報: [finished] org.tanoseam.examples.ContainerControlTest#testScope
ShoppingCart::destroy
Logger::destroy

上のサンプルコードでは、ApplicationScopeのLoggerも解放してしまっていますが、本来であれば、実際の動きに近づけるためにApplicationScopeのBeanについてはメソッドの先頭で生成し、メソッド終了時に解放したいところです。次の例は、RequestScopeとSessionScopeのコンテキストを個別に制御することで、ApplicationScopeを停止しないようにしています。

	@Test
	public void testScope() {	
		shoppingCart.put(new Product("001", 1));
		shoppingCart.put(new Product("002", 3));
		shoppingCart.put(new Product("003", 7));
		logger.info("ShoppingCart=>" + shoppingCart);
		contextControl.stopContext(RequestScoped.class);
		contextControl.stopContext(SessionScoped.class);
		contextControl.startContext(SessionScoped.class);
		contextControl.startContext(RequestScoped.class);
		logger.info("ShoppingCart=>" + shoppingCart);
		assertTrue(shoppingCart.isEmpty());
	}

以下がこのテストのコンソール出力です。ApplicationScopeのLoggerについて、Logger:initializeが先頭で、Logger.destroyが最後に出力されています。

11 24, 2015 11:40:26 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted
情報: [run] org.tanoseam.examples.ContainerControlTest#testScope
Logger::initialize
ShoppingCart::initialize
ShoppingCart=>ShoppingCart [products=[Product [productId=001, quantity=1], Product [productId=002, quantity=3], Product [productId=003, quantity=7]]]
ShoppingCart::destroy
ShoppingCart::initialize
ShoppingCart=>ShoppingCart [products=[]]
11 24, 2015 11:40:26 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished
情報: [finished] org.tanoseam.examples.ContainerControlTest#testScope
ShoppingCart::destroy
Logger::destroy

CDIによるプロパティのインジェクション

アプリケーションの設定情報をソースコード中にハードコードするのではなく、プロパティファイルに記述しておいて後から変更できるようにしたい場合があります。このプロパティの値をCDIらしくインジェクションで取得する方法としてProducerを使う方法があります。こうやって手作りするのは良いのですが、複数の変数の型に対応させたり、拡張可能にしたりと機能を盛り込んでいくと手間がかかります。今回はこのような機能を提供するDeltaSpike Configurationについて紹介します。

DeltaSpike Configurationとは

DeltaSpike Configurationはプロパティから構成情報を取得するための機能でCoreモジュールに含まれます。アプリケーションからこれを使うのは簡単です。変数のインジェクション時に@ConigurePropertyというアノテーションを指定すると、nameで指定された名前に応じたプロパティの値を変数にインジェクションしてくれます。

@Inject
@ConfigProperty(name = "serverAddress")
String serverAddress;

デフォルト値を設定したい場合はdefaultValueも一緒に指定します。

@Inject
@ConfigProperty(name = "serverAddress" , defaultValue = "localhost")
String serverAddress;

プロパティファイルのデフォルトはMETA-INF/apache-deltaspike.propertiesです。この例では、このファイルには以下のような該当するプロパティを書いておきます。

serverAddress=192.168.0.1

実は、DeltaSpike Configurationでは、apache-deltaspike.properties以外の場所でもプロパティを設定することが可能です。プロパティを設定をする方法として以下が提供されています。複数同時に設定されている場合はordinalが大きい方が優先されます。

表1:DeltaSpike Configurationのプロパティ設定箇所
プロパティのソース ordinal 備考
システムプロパティ 400
環境変数 300
JNDI 200 “java:comp/env/deltaspike/”
プロパティファイル 100 “META-INF/apache-deltaspike.properties”

@ConfigPropertyでサポートされる型

@ConfigPropertyでサポートされる変数の型は以下になります。

  • String
  • Integer
  • Long
  • Float
  • Double
  • Boolean

カスタムの型に対応させたい場合はConfigResolver.Converterインタフェースを実装します。実装例はドキュメントを参照してください。

@ConfigPropertyの例

プロパティを取得する具体例を見てみましょう。次のような文字列と数値のプロパティをあらかじめMETA-INF/apache-deltaspike.propertiesに設定しておきます。

username=tanoseam
pollingInterval=120

以下のテストコードを実行してみます。2番目、3番目の@ConfigPropertyの型はStringではなくIntegerです。このように文字列以外の値についてはプロパティ値をターゲットの変数の型に変換してからインジェクションしてくれるので便利です。

@RunWith(CdiTestRunner.class)
@TestControl(projectStage = ProjectStage.UnitTest.class)
public class ConfigurationTest {

	@Inject
    @ConfigProperty(name = "username", defaultValue = "tanoseam")
    String username;

    @Inject
    @ConfigProperty(name = "pollingInterval", defaultValue = "60")
    Integer pollingInterval;
    
    @Inject
    @ConfigProperty(name = "nullValue")
    Integer nullValue;

	@Test
	public void testConfig() {
		assertEquals("tanoseam", username);
		assertEquals(120, pollingInterval.intValue());
		assertNull(nullValue);
    }
}

このテストは問題なく成功するはずです。

プロパティの優先順位を確認するため、テスト起動時にシステムプロパティとして別の値を設定してみましょう。

mvn test -Dusername=neverbird

こうすると、今度はテストが失敗します。システムプロパティの方がプロパティファイルよりも優先されてインジェクトされているのがわかります。

Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.475 sec <<< FAILURE!
testConfig(org.tanoseam.examples.ConfigurationTest)  Time elapsed: 0.007 sec  <<< FAILURE!
org.junit.ComparisonFailure: expected:<[tanoseam]> but was:<[neverbird]>
	at org.junit.Assert.assertEquals(Assert.java:115)
	at org.junit.Assert.assertEquals(Assert.java:144)
	at org.tanoseam.examples.ConfigurationTest.testConfig(ConfigurationTest.java:33)

 

CDIを使ったプログラムの単体テスト(3)

CDIを使った単体テストについての3回目はDeltaSpike Test-ControlモジュールのMockフレームワークの使い方について紹介します。前回のブログでは@ExcludeとProjectStageを組み合わせることで開発ステージに合わせてインジェクトするBeanを切り替える方法について説明しましたが、今回紹介する方法ではもっとダイレクトにMockを使ってインジェクトの対象を切り替えます。

Mockフレームワークとは

使い方を説明するために、まずインジェクションを実行する舞台となるBeanを定義します。サンプルコードとしてMyLogicというBeanを作ります。このBeanはget()メソッドの引数の10倍の数を返すもので、内部でRepositoryのsave()メソッドを呼び出します。

package org.tanoseam.examples;

import javax.enterprise.context.SessionScoped;
import javax.inject.Inject;
import java.io.Serializable;

@SessionScoped
public class MyLogic implements Serializable {
	
	public MyLogic() {}
	
	@Inject
	Repository repository;
	
	public int get(int value) {
		int result = value * 10;
		repository.save("something");
		return result;
	}
}

Repositoryは次のようなシンプルなインタフェースです。

package org.tanoseam.examples;

public interface Repository {
	public void save(Object obj);
}

そして、これを実装するRdbRepositoryというBeanを定義します。メソッドが呼び出されたことだけが確認できれば良いのでメソッドの内容はプリント文だけにしておきます。

package org.tanoseam.examples;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class RdbRepository implements Repository {
	public void save(Object dummy) {
		System.out.println("RdbRepository is called");
	}
}

このようなケースにおいてMyLogicからRdbRepositoryをインジェクトするのではなく、 テスト時にはその代わりにMockのオブジェクトをインジェクトするようにします。

DeltaSpikeのCdiTestRunnerでMockを扱うにはDynamicMockMangerを使います。これには手でMockオブジェクトを定義する方法とMockフレームワークを使う方法があります。

手でMockオブジェクトを定義する方法

次のサンプルコードは、手でMockオブジェクトを定義する方法です。DynamicMockMangerのaddMock()メソッドを使って、既存のクラスであるRdbRepositoryの無名サブクラスを定義します。これで、RdbRepositoryのインスタンスの代わりに、この無名クラスのインスタンスがインジェクトされるようになります。

@RunWith(CdiTestRunner.class)
public class DynamicMockManagerTest {
	
	@Inject
	MyLogic mylogic;
	
	@Inject
    DynamicMockManager mockManager;

	@Test
	public void testMock() {
        mockManager.addMock(new RdbRepository() {
        	@Override
        	public void save(Object obj) {
                System.out.println("Mock of RdbRepository is called");
            }
        });
		assertEquals(100, mylogic.get(10));
    }
}

Mockの使用を有効にする

DeltaSpikeではこのMockの機能はDELTASPIKE-872のためにデフォルトでは有効になっていません。使えるようにするためには以下の2つのシステムプロパティを設定することで有効になります。

  • deltaspike.testcontrol.mock-support.allow_mocked_beans=true
  • deltaspike.testcontrol.mock-support.allow_mocked_producers=true

別の方法としては、META-INF/apache-deltaspike.propertiesに上記のプロパティを設定するのでも構いません。

それから必要なのはpom.xmlの設定ですが、このDynaimcMockMangerはDeltaSpike Test-Controlモジュールに含まれるので、前々回のブログと同じpom.xmlの設定で実行することができますのでそちらを参照してください。

テストの実行ログの抜粋は以下のようになります。

11 04, 2015 9:47:08 午後 org.jboss.weld.environment.se.WeldContainer initialize
INFO: WELD-ENV-002003: Weld SE container STATIC_INSTANCE initialized
11 04, 2015 9:47:08 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testStarted
情報: [run] org.tanoseam.examples.DynamicMockManagerTest#testMock
Mock of RdbRepository is called
11 04, 2015 9:47:08 午後 org.apache.deltaspike.testcontrol.api.junit.CdiTestSuiteRunner$LogRunListener testFinished
情報: [finished] org.tanoseam.examples.DynamicMockManagerTest#testMock
11 04, 2015 9:47:08 午後 org.jboss.weld.environment.se.WeldContainer shutdown
INFO: WELD-ENV-002001: Weld SE container STATIC_INSTANCE shut down

Mockitoを使う方法

DynamicMockManagerはMockitoと組み合わせて使うこともできます。Mochitoを使うにはpom.xmlに依存ライブラリを追加してください。

		<dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>

次の例では、Mochitoを使って引数10を与えた場合に戻り値として99を返すMockオブジェクトを定義しています。

	@Test
	public void testMockito() {
		MyLogic mockedLogic = mock(MyLogic.class);
		when(mockedLogic.get(10)).thenReturn(99);
		mockManager.addMock(mockedLogic);

		assertEquals(99, mylogic.get(10));
    }

DeltaSpikeにおけるMock使用上の注意

DeltaSpikeのMockを使ったテストの方法はDELTASPIKE-872に記載されているように、DeltaSpike 1.5.1では、インターセプターを使ったBeanでは適用することができないという問題が報告されています。このような制限はあるものの、単体テストでMockを使うのは非常に有効な方法だと思いましたので、このブログで紹介することにしました。

%d人のブロガーが「いいね」をつけました。