業務 アプリケーション は一般に,特定の業種をターゲットにし,GUIを複数ユーザが同時利用し,数多くのエンティティ(テーブル)を持つデータベースを持ち,さらには組織内外の他のアプリケーションと連携することも珍しくありません. Javaでは,Java EEやSpring Frameworkといったものがよく使われます.
この章では,コンテナ外の結合テストによる,業務アプリケーションのテスト手法を説明します. いわゆる ビジネスシナリオ,あるいはユースケースや "利用シナリオと呼ばれるものの1ステップをテストするためのものです. 典型的な階層化アーキテクチャでは,最上位のレイヤ(ふつうアプリケーション層)のコンポーネントから下位のレイヤのpublicメソッドを呼び出します.
デモンストレーションのために,Java EEで作ったSpring Pet Clinicというサンプルアプリケーションを使います. 完全なコードはプロジェクトのリポジトリにあります. アプリケーションは4つのレイヤで構成されています.UI(プレゼンテーション)レイヤ,アプリケーションレイヤ,ドメインレイヤ,インフラレイヤの4つです.
このアプリケーションのドメインモデルには,6つのドメインエンティティがあります: Vet
(獣医師),Specialty
(診療科),Pet
,PetType
,Owner
(飼い主),Visit
(訪問). (※用語や考え方は Domain Driven Designを参照)
エンティティのほかに,アプリケーションのドメインモデル(及びレイヤ)にはドメインサービスクラスもあります.
このシンプルなドメインでは,1つのエンティティタイプに1つのクラスだけを定義します(VetMaintenance
やPetMaintenance
等).
DDD(ドメイン駆動設計)では,エンティティは「リポジトリ」コンポーネントによって永続化ストレージに追加・変更・削除されます.
JPAのような洗練されたORM APIを使えば,リポジトリコンポーネントは1つで済みます.ドメインやアプリケーション固有ではないインフラレイヤの Database
クラスです.
アプリケーションはリレーショナル・データベースを使用します.サンプルアプリケーションではインメモリ型のHSQLDBデータベースを使用しており,アプリケーション内にデータベースを内包しています.
アプリケーションレイヤにはアプリケーションサービスクラスを含みます.これは,UIからのユーザの入力を解釈して下位レイヤを呼び出したり,出力結果をUIに表示したりします. このレイヤはデータベースのトランザクション境界(単位)となります.
Java EE 7では,ドメインの@Entity
型にJPA,ドメインサービスにEJB(ステートレスなSessionBean)またはシンプルな@Transactional
クラス,アプリケーションサービスにはJSF @ViewScoped
Beanを使用します.
サンプルにはUIレイヤのコードは含まれていません(結合テストの対象ではないため).
(※Java EEでは,UIレイヤは".xhtml"形式のJSF faceletsで構成されます.)
最初の結合テストでは,獣医師(Vet)画面上で,すべての獣医師とその診療科を表示することを考えてみましょう.
public final class VetScreenTest
{
@TestUtil VetData vetData;
@SUT VetScreen vetScreen;
@Test
public void findVets() {
// 入力としてデータベースに獣医師と診療科のデータを登録する
Vet vet2 = vetData.create("Helen Leary", "radiology");
Vet vet0 = vetData.create("James Carter");
Vet vet1 = vetData.create("Linda Douglas", "surgery", "dentistry");
List<Vet> vetsInOrderOfLastName = asList(vet0, vet1, vet2);
// テスト対象のコードを実行する (VetScreen, VetMaintenance, Vet, Specialty).
vetScreen.showVetList();
List<Vet> vets = vetScreen.getVets();
// 出力結果が期待値通りか検証する
vets.retainAll(vetsInOrderOfLastName);
assertEquals(vetsInOrderOfLastName, vets); // リストの内容と順序をチェック
Vet vetWithSpecialties = vets.get(1); // "vet1"は...
assertEquals(2, vetWithSpecialties.getNrOfSpecialties()); // ...2つの診療科を担当
vetData.refresh(vetWithSpecialties); // DBに存在する獣医師のデータを再取得
List<Specialty> specialtiesInOrderOfName = vetWithSpecialties.getSpecialties();
assertEquals("dentistry", specialtiesInOrderOfName.get(0).getName()); // 診療科をチェック...
assertEquals("surgery", specialtiesInOrderOfName.get(1).getName()); // ...正しい順序であること
}
}
最初に注意すべき点は,このテストはアプリケーションの最上位層として実行されており,Javaで書かれている―つまり,アプリケーションはJVM上で動作しているということです. ですから,HTTPリクエスト・レスポンスや,URLがアプリケーションにどうマッピングされているか,といった点は無視できます.そうした細かい点は,UIの実装に使われる技術(JSF, JSP, Struts, GWT, Spring MVC等)によって異なってくるため,結合テストでは考えないようにします.
次に注意しなければならないのは,テストコードは非常にクリーンで,テスト対象が明確になっていることです. テストはアプリケーションや業務ドメインについてはきちんと記述している一方で,低レベルなデプロイメントやデータベース設定やトランザクションといった点は一切記述していません.
最後に注意すべき点は,JMockit APIをテストクラス内で全く意識していないことです.
使ったのは@TestUtil
と@SUT
のアノテーションのみです.
これらはユーザ定義のアノテーションで,チーム内で任意の名前にできます.サンプルコートでは,次のように定義しました.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Tested(availableDuringSetup = true, fullyInitialized = true)
public @interface TestUtil {} // テストユーティリティ
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Tested(fullyInitialized = true)
public @interface SUT {} // テスト対象のシステム
これにより,JMockitの @Tested
アノテーションは メタアノテーションとして使われています.
"fullyInitialized = true
" と記述すると,Dependency Injection(依存性注入)によって対象クラスの依存関係が(コンストラクタとそこに含まれるフィールドのインジェクションにより)自動解決されます.
"availableDuringSetup = true
" は,テスト対象のオブジェクトを @Before
(Junit) や @BeforeMethod
(TestNG) で指定したセットアップメソッドを実行するより前に 生成します.指定しない場合,デフォルトではセットアップメソッドの後,各テストメソッドを実行する前にオブジェクトを生成します.
サンプルのテストクラスでは,これらの効果は特に活用しておらず,単にフィールドの意味や目的を明文化しただけになっています.
テストクラスから分かるように,VetData
はテストに必要なデータを作成したり,データベースから最新のエンティティをロードする refresh(an entity)
メソッドがあったりします.
その名前が示すように,テストスイートはエンティティタイプごとにこうしたクラスを持ち,永続化インスタンスが必要なテストクラスで使用されます.
詳細は次のセクションで説明します.
VetData
のようなユーティリティクラスは次のようになっています:
public final class VetData extends TestDatabase
{
public Vet create(String fullName, String... specialtyNames) {
String[] names = fullName.split(" ");
Vet vet = new Vet();
vet.setFirstName(names[0]);
vet.setLastName(names[names.length - 1]);
for (String specialtyName : specialtyNames) {
Specialty specialty = new Specialty();
specialty.setName(specialtyName);
vet.getSpecialties().add(specialty);
}
db.save(vet);
return vet;
}
// 必要に応じて異なるデータを取る "create" メソッド を記述する
}
こうしたクラスは簡単に記述できます.既存のエンティティクラスを使い,親クラス TestDatabase
のフィールド "db
" のメソッドを使うだけです.
これは,永続化にJPA,そして結合テストにJMockitを使っている限り,他の業務アプリケーションでも再利用可能な共通クラスと言えます.
public class TestDatabase
{
@PersistenceContext private EntityManager em;
@Inject protected Database db;
@PostConstruct
private void beginTransactionIfNotYet() {
EntityTransaction transaction = em.getTransaction();
if (!transaction.isActive()) {
transaction.begin();
}
}
@PreDestroy
private void endTransactionWithRollbackIfStillActive() {
EntityTransaction transaction = em.getTransaction();
if (transaction.isActive()) {
transaction.rollback();
}
}
// 他のユーティリティメソッド(refresh, findOne, assertCreated等)
}
本番環境でも利用できる Database
ユーティリティクラスは,JPAのEntityManager
クラスをより使いやすくしたものです.
利用は任意です.
テストでは,"db
"フィールドの代わりに"em
"フィールドを直接使うこともできます.
EntityManager em
フィールドは,テストのランタイムクラスパス(Maven互換のプロジェクト階層であれば"src/test",ちなみに本番環境向けには"src/main")にあるMETA-INF/persistence.xml
ファイルに従ってインスタンスは自動的に生成・注入(inject)されます.
EntityManagerのデフォルトインスタンスは1つだけ生成され,テストまたは本番環境の @PersistenceContext
フィールドを持つクラス(Database
とか)に注入されます.
複数のデータベースが必要な場合,異なるEntityManagerを持つように,アノテーションの"name
"属性を設定し,それに対応するエントリをpersistence.xml
ファイルに記述します.
このクラスのもう1つの重要な役割は,テストごとにトランザクションを分離するとともに,テスト開始前にトランザクションを開始し,テスト終了後に(テストが成功しようが失敗しようが)ロールバックして終了するよう保証することです.
これは,JMockitが
@PostConstruct
メソッドと@PreDestroy
メソッド(javax.annocation
標準APIのもの.Spring Frameworkでもサポート)を適切なタイミングで実行することで機能しています.
テストデータのオブジェクトは,@Tested(availableDuringSetup = true)
フィールドのテストクラスに設定され,セットアップメソッドやテストメソッドの前に「構築」され,テスト終了時に「破棄」されます.
@Autowired
や @Value
といったSpring固有のアノテーションも,
@Tested
オブジェクトでサポートされています.
ただし,Springベースのアプリケーションでは
BeanFactory
実装クラスのインスタンスで BeanFactory#getBean(...)
メソッド を直接呼び出すこともできます.
Beanファクトリインスタンスの取得方法にかかわらず,
@Tested
オブジェクトと
@Injectable
オブジェクトはBeanファクトリインスタンスからBeanとして利用できます.
mockit.integration.springframework.FakeBeanFactory
フェイククラスを適用することで,JUnitを使って下記のようにします
public final class ExampleSpringIntegrationTest
{
@BeforeClass
public static void applySpringIntegration() {
new FakeBeanFactory();
}
@Tested DependencyImpl dependency;
@Tested(fullyInitialized = true) ExampleService exampleService;
@Test
public void exerciseApplicationCodeWhichLooksUpBeansThroughABeanFactory() {
// テスト対象のコード:
BeanFactory beanFactory = new DefaultListableBeanFactory();
ExampleService service = (ExampleService) beanFactory.getBean("exampleService");
Dependency dep = service.getDependency();
...
assertSame(exampleService, service);
assertSame(dependency, dep);
}
}
Beanファクトリのフェイクを適用すると,テストクラスのフィールドにあるテスト対象のオブジェクトは,Bean名とフィールド名が一致していれば,
任意のSpring BeanファクトリインスタンスのgetBean(String)
を実行して自動的に返却します.
さらに,テストクラスから@Tested
オブジェクトとして指定することで,mockit.integration.springframework.TestWebApplicationContext
クラスは,org.springframework.web.context.ConfigurableWebApplicationContext
実装クラスとして利用することができます.
一部のアプリケーションコードでは,アプリケーション固有の実装クラスの多くにインタフェースを使用します.
これらのインタフェースは,注入された依存関係を受け取るフィールドやパラメタで使用されるものです.
そのため,インタフェースベースの依存関係を持つ@Tested
オブジェクトをインスタンス化するときは,
インタフェースを実装するクラスをJMockitに教える必要があります.方法は2つあります.
このテスト手法では,業務アプリケーションのJavaコードすべてを網羅する結合テストを行うことをゴールにしています. TomcatやGlassfish, JBoss WildflyといったJavaアプリケーションサーバで実行することによる固有の問題を避けるため,テストコード・本番コードが同じJVMインスタンスで実行されるコンテナ外テストとしています.
テストはアプリケーションの最上位コンポーネントのAPIに対して記述されています. そのため,Java以外で記述されたり,まちまちなテンプレート言語で記述されるようなUIのコードはテストされていません. JavaScriptを含むようなWebアプリケーションのUIコンポーネントをテストするには,WebDriverやHtmlUnit APIを使い,HTTPリクエストとレスポンスをベースにした機能のテストを記述する必要があります. こうしたテストはコンテナ内の手法で,アプリケーションサーバの起動方法やデプロイ方法,テストの独立性を維持する方法,あるいはデータベースのトランザクションがコミットされてしまうなど,様々な問題や難しさをはらんでいます.
対照的に,コンテナ外の結合テストは,ちょうど良い粒度で,テスト終了後に常にロールバックされるトランザクション構成されています. この手法は,作成が容易で,かつ実行が早く(特に起動コストが小さい),堅牢なテストを可能にします. さらに,単一のJVMインスタンスで実行されるため,コードカバレッジツールやデバッガの利用が容易です. 欠点としては,UIテンプレートコードやクライアントサイドで動作するJavaScriptコードなどがテストで網羅できないことが挙げられます.
翻訳: たいぷらいたー(にゃみかん),ライセンス表示: LICENSE.txt