SeamFramework.orgCommunity Documentation
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();
}
...
}
統合テスト環境では準備できないようなリソースを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は モック実装を優先します。 本番環境ではモック実装は存在しないので、実際のコンポーネントが実行されます。
難しいのはユーザーインタラクションをどのようにエミュレートするかです。そしてどこにアサーションを置くかです。あるテストフレームワークでは、すべてのアプリケーションをテストするのに、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など)のテスト環境を設定する必要があります。
まず、最低限必要な依存関係を見てみましょう。
表 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
ディレクトリに置いてください。
SeamではTestNGであればすぐに使用できますが、JUnitなどの別のテストフレームワークを利用することもできます。
以下の要領でAbstractSeamTest
を実装してください。
すべてのテストメソッドの前にsuper.begin()
を呼ぶ。
すべてのテストメソッドの後にsuper.end()
を呼ぶ。
統合テスト環境をセットアップするsuper.setupClass()
を呼ぶ。テストメソッドのどれかが呼ばれる前に呼ばれるようにしてください。
統合テスト環境を消去するsuper.cleanupClass()
を呼ぶ。
統合テストの開始時にsuper.startSeam()
を呼び、Seamを起動する。
統合テストの終了時にsuper.stopSeam()
を呼び、Seamを停止する。
各テストの前にデータベースにデータを挿入したり、消去したりしたい場合はDBUnitと連携します。 SeamTest
の替わりに DBUnitSeamTest
を継承してください。
DBUnitのデータセットを記述しなければならりません。
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 を参照してください。
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コンポーネントのレンダリングはサポートしませんので、メールボディをテストするのは簡単ではありません。