SoftwareEngineering/Java/UnitTest/JMockit

エンタープライズアプリケーションのテスト

エンタープライズ アプリケーションは、通常は複数の同時ユーザー用のGUIと多数のエンティティタイプ用のアプリケーションデータベースを持つ、特定のビジネスドメインを対象としています。
また、組織内外の他のアプリケーションと統合されることもあります。
Javaでは、Java EE APIおよび/またはSpringフレームワークは、通常、そのようなアプリケーションを構築する際に使用されます。

この章では、アウト・オブ・コンテナの統合テストを記述してJavaエンタープライズ・アプリケーションをテストするアプローチについて説明します。
各テストでは、明確なビジネス・シナリオ (「 ユース・ケース 」または「 使用 」シナリオとも呼ばれます) )。
典型的な階層化アーキテクチャでは、このようなテストでは最上位層(通常はアプリケーション層)のコンポーネントからパブリックメソッドが呼び出され、下位層が呼び出されます。

デモンストレーションのために、Java EEバージョンのSpring Pet Clinicサンプルアプリケーションを使用します。
フルコードはプロジェクトリポジトリで利用できます。
アプリケーションコードベースは、UIまたはプレゼンテーション層、アプリケーション層、ドメイン層、およびインフラストラクチャ層の4つの層で構成されています。

Vet (獣医師)、Specialty (獣医専門)、Pet 、PetType 、Owner (ペット所有者)、Visit ( Visit者)の6つのドメインエンティティがあります( Domain Driven Designのアプローチと用語にPetType )ペットとその所有者を診察室に連れて行く)。
エンティティのほかに、アプリケーションのドメインモデル(およびレイヤー)にはドメインサービスクラスも含まれます。
この単純なドメインでは、各エンティティタイプ( VetMaintenance 、PetMaintenanceなど)ごとに1つのクラスしかありません。

DDDでは、エンティティは「リポジトリ」コンポーネントを介して永続ストレージに追加され、永続ストレージから再構成または削除されます。
洗練されたORM API(JPA)を使用すると、そのようなリポジトリはドメインまたはアプリケーション固有ではないため、インフラストラクチャ層に入ります: Databaseクラス。
このアプリケーションでは、リレーショナルデータベース、特にサンプルアプリケーション内のメモリ内のHSqlDbデータベースを使用するため、自己完結型にすることができます。

アプリケーションレイヤーには、UIからのユーザー入力を下位レイヤーへの呼び出しに変換し、出力データをUIで表示できるようにするアプリケーションサービスクラスが含まれています。
これは、データベーストランザクションが画定される層です。

Java EEの使用

Java EE 7では、ドメイン@Entityタイプ、EJB(ステートレスセッションBean)、またはドメインサービス用の@TransactionalクラスのJPA、およびアプリケーションサービスのJSF @ViewScoped Beanを使用しています。
UIレイヤーのコードは、サンプルには含まれていません。なぜなら、統合テストスイートでは実行されないからです。(Java EEでは、このレイヤーは ".xhtml"ファイル形式のJSFフェイスレットで構成されます)。

私たちの最初の統合テストクラスでは、Vet画面を考えてみましょう.Vet画面では、すべての獣医の専門分野の一覧を表示します。

public final class VetScreenTest
{
   @TestUtil VetData vetData;
   @SUT VetScreen vetScreen;

   @Test
   public void findVets() {
      // Inserts input data (instances of Vet and Specialty) into the database.
      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);

      // Exercises the code under test (VetScreen, VetMaintenance, Vet, Specialty).
      vetScreen.showVetList();
      List<Vet> vets = vetScreen.getVets();

      // Verifies the output is as expected.
      vets.retainAll(vetsInOrderOfLastName);
      assertEquals(vetsInOrderOfLastName, vets); // checks the contents and ordering of the list

      Vet vetWithSpecialties = vets.get(1); // this will be "vet1"...
      assertEquals(2, vetWithSpecialties.getNrOfSpecialties()); // ...which we know has two specialties

      vetData.refresh(vetWithSpecialties); // ensures the Vet contains data actually in the db
      List<Specialty> specialtiesInOrderOfName = vetWithSpecialties.getSpecialties();
      assertEquals("dentistry", specialtiesInOrderOfName.get(0).getName()); // checks that specialties...
      assertEquals("surgery", specialtiesInOrderOfName.get(1).getName()); // ...are in the correct order
   }
}

上記のテストで最初に気づくのは、アプリケーションの最上位層から始まります。
これはJavaでコード化されているため、JVMで完全に実行されます。アプリケーション層。
したがって、このテストでは、HTTPリクエストやレスポンス、アプリケーションURLがアプリケーション層のコンポーネントにどのようにマッピングされるかは関係ありません。
そのような詳細は、UI実装(JSF、JSP、Struts、GWT、Spring MVCなど)に使用される技術によって異なり、統合テストの対象外と考えられます。

もう1つ注目すべきことは、テストコードは非常にきれいで、テストしようとしているものに集中していることです。
デプロイメント、データベース構成、トランザクションなどの低レベルの懸念なしに、アプリケーションとそのビジネスドメインの観点から完全に書かれています。

最後に、注目すべき第3の点は、テストクラスにJMockit APIがまったく表示されないことです。
私たちが持っているのは@TestUtilと@SUTアノテーションの使用だけです。
これらはユーザー定義のアノテーションで、チームのプリファレンスに応じて任意の名前が選択されます。
サンプルコードでは、次のように定義されています。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Tested(availableDuringSetup = true, fullyInitialized = true)
public @interface TestUtil {} // a test utility object

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Tested(fullyInitialized = true)
public @interface SUT {} // the System Under Test

ここでは、JMockitの@Tested注釈をメタ注釈として使用します。
" fullyInitialized = true "を使用すると、テスト対象クラスの依存関係が依存関係注入(具体的には、コンストラクタ注入とそれに続くフィールド注入)によって自動的に解決されます。"
availableDuringSetup = true "を使用すると、各テストメソッドの直前にテストされたオブジェクトを作成するデフォルトとは対照的に、
テストセットアップメソッド(JUnitの@BeforeMethodまたはTestNGの@BeforeMethod )が実行される前に、実行し、そして任意のセットアップ方法の後に。
この最初のテストクラスの例では、このエフェクトは使用されないため、" @TestUtil "を使用する唯一のメリットは、テストクラスのフィールドのインテントを文書化することです。

テストで見られるように、VetDataは、テストに必要な新しいテストデータを作成するメソッドと、refresh(an entity) (特定のエンティティの永続的な状態をデータベースから新しくロードするrefresh(an entity)などの他のユーティリティメソッドを提供します。
名前が示すように、テストスイートは、各エンティティタイプに対して、そのタイプの永続インスタンスが必要なときはいつでも、1つ以上のテストクラスで使用されるクラスを1つ持ちます。
詳細については、次のセクションで説明します。

インフラストラクチャのテスト

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;
   }

   // other "create" methods taking different data items, if needed
}

そのようなクラスは、既存のエンティティクラスだけでなく、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();
      }
   }

   // Other utility methods: "refresh", "findOne", "assertCreated", etc.
}

JPAのEntityManagerよりも使いやすいAPIを提供するDatabaseユーティリティー・クラス(これも使用可能で実動コードで使用されていますが、その使用はオプションです)。
テストでは " db "の代わりに " em "フィールドを直接使用することができます(もちろんprotected )。
テスト・データベース・クラスのEntityManager emフィールドには、テスト・ランタイム・クラスパスに存在するMETA-INF/persistence.xmlファイルに従って自動的に作成されたインスタンスが挿入されます(使用時には「src / test」ディレクトリに移動します)。
Mavenと互換性のあるプロジェクト構造です;ファイルの "制作"バージョンは "src / main"の下に提供されます)。
単一のデフォルトエンティティマネージャインスタンスが作成され、テストクラスまたは実働クラス( Databaseクラスなど)のいずれかに@PersistenceContextフィールドが挿入されます。
複数のデータベースが必要な場合は、それぞれ、この注釈のオプションの「 name 」属性で構成された異なるエンティティ・マネージャと、persistence.xmlファイル内の対応するエントリを持ちます。

この基本クラスのもう一つの重要な責務は、各テストが実行されるトランザクションを画定し、テストが開始される前に存在することを保証し、テストが完了した後(成功または失敗のいずれかで)ロールバックで終了することです。
これは@PostConstructが適切な時に@PreDestroyと@PreDestroyメソッド(標準のjavax.annotation APIから、またSpringフレームワークでもサポートされている)を実行するためです。
各 "テストデータ"オブジェクトは@Tested(availableDuringSetup = true)フィールドのテストクラスに導入されるため、セットアップまたはテストメソッドの前に "構築"され、各テストが終了すると "破棄"されます。

Springフレームワークの使用

完全に初期化された@Testedオブジェクトでは、@Value @Autowiredや@ValueなどのSpring固有のアノテーションもサポートされています。
しかし、Springベースのアプリケーションは、さまざまなBeanFactory実装クラスのインスタンス上で、BeanFactory#getBean(...)メソッドを直接呼び出すこともできます。

Beanファクトリインスタンスがどのように取得されたかにかかわらず、@Tested @Injectableオブジェクトと@Injectableオブジェクトは、JUnitを使用して以下に示すように、
単純にmockit.integration.springframework.BeanFactoryMockUpモックアップクラスを適用することによって、BeanファクトリインスタンスからBeanとして利用できるようになります。

public final class ExampleSpringIntegrationTest
{
   @BeforeClass
   public static void applySpringIntegration() {
      new BeanFactoryMockUp();
   }

   @Tested DependencyImpl dependency;
   @Tested(fullyInitialized = true) ExampleService exampleService;

   @Test
   public void exerciseApplicationCodeWhichLooksUpBeansThroughABeanFactory() {
      // In code under test:
      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)呼び出しから自動的に返されます。

さらに、mockit.integration.springframework.TestWebApplicationContextクラスは、
テストクラスから@Testedオブジェクトを公開するorg.springframework.web.context.ConfigurableWebApplicationContext実装として使用できます。

アプローチのトレードオフ

このテスト手法では、エンタープライズアプリケーションのコードベースにあるすべてのJavaコードを対象とした統合テストを行うことが目標です。
Javaアプリケーションサーバー(Tomcat、Glassfish、JBoss Wildflyなど)の内部でコードを実行することの難しさを避けるために、
これらのコードはすべてのコード(生産コードとテストコード)が同じJVMインスタンス。

テストは、アプリケーションの最上位コンポーネントのAPIに対して実行されます。
そのため、UIコードは通常Java言語で書かれていないため、JSFのfaceletsやJSPなどの技術固有のテンプレート言語やSpringフレームワークでサポートされているものなどでは実行されません。
このようなUIコンポーネントを使用するには、WebアプリケーションでJavaScriptコードが頻繁に使用されることが多いため、WebDriverやHtmlUnitなどのテストAPIを使用して、
HTTP要求と応答に基づいて機能的な UIテストを作成する必要があります。
このようなテストには、コンテナ内でのアプローチが必要です。アプリケーションサーバーの起動方法、起動方法、アプリケーションコードの展開方法と再配置方法、
テストを互いに分離した状態に保つ方法一般的な機能テストでは、1つまたは複数のデータベーストランザクションを実行することが多く、そのうちの一部またはすべてが通常コミットされます。

これと比較して、ここに示されているアウトオブコンテナ統合テストはより細分化されており、通常、テストの最後に常にロールバックされる単一のトランザクションを備えています。
このアプローチでは、作成が簡単で実行速度が速く(特に起動コストが無視できる)、脆弱なテストが可能です。
すべてが単一のJVMインスタンス内で実行されるため、コードカバレッジツールを使用してデバッガを使用する方が簡単で簡単です。
欠点は、UIテンプレートのコードとクライアントサイドのJavaScriptコードは、このようなテストでカバーされないことです。


トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2018-03-24 (土) 20:09:14 (628d)