読者です 読者をやめる 読者になる 読者になる

エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaEE7をはじめよう(17) - CDIのユニットテスト

JavaEE CDI

CDIを使用したクラスのユニットテスト、すなわちJUnitなどのテストツールを使用した自動テストはどのように行うべきだろうか。 今回はユニットテストの方法について紹介する。

テスト対象のクラス

今回テストを行う対象は、前回の記事で紹介した、DAOを通じてデータベースを操作するSampleServiceである。

以下に再掲する。

@Transactional 
@RequestScoped
public class SampleService {
    
    // DAOをインジェクション
    @Inject
    private TeamDao dao;
    
    /**
     * 新しいチームを登録する
     * 
     * 空文字の場合は実行時例外を、
     * 引数が2文字以下の場合はチェック例外を発生させる。
     * @param teamName チーム名
     * @throws SampleException チーム名が2文字以下の場合
     */
    public void newTeam(String teamName) throws SampleException {
        
        Team t = new Team();
        t.setName(teamName);
        dao.register(t);
        
        // 永続処理の実行後に、例外判定を行う。
        if (teamName.equals("")) {
            throw new RuntimeException("uncheck exception throws.");
        }
        if (teamName.length() <= 2) {
            throw new SampleException("check exception throws.");
        }
    }

    
    public List<Team> allTeams() {
        return dao.findAll();
    }
}

CDI単体テストを容易にする

このSampleServiceTeamDaoを利用しているため、TeamDaoに依存している。 また、TeamDaoもデータベースにアクセスするためにEntityMangerなどに依存している。

ただし、依存しているとは言っても、SampleServiceTeamDaoのフィールドを宣言しているだけで、依存しているインスタンスを生成する処理は一切行っていない。

もし、依存しているインスタンスの生成をSampleService内で行っていると、依存しているインスタンスの中身や初期化方法まで考慮する必要があるため、ユニットテストは非常に煩雑になる。 依存先ではデータベースにアクセスしているため、考慮すべきことはさらに増える。

SampleService は様々なアノテーションが付いていることを除けば、フレームワーク特有のクラスなどの継承も行っておらず、依存するオブジェクトの生成・取得も行っていない、単なるPOJOである。 このため、アノテーションの問題をクリアできれば、単にSampleServiceコンストラクタを呼び出してインスタンスを生成しても問題は生じない。Servletのようなフレームワークなどから提供されるクラスを継承していると、そもそもインスタンス生成すら困難なケースがあるため、普通にインスタンスの生成ができることはユニットテストでは重要だ。

もちろんCDIによるインスタンス生成を無効にした場合、フィールドのTeamDaonullのままなので、メソッドの実行はできなくなる。このため、POJOが依存しているフィールドにオブジェクトを設定する必要がある。 具体的な方法としては、依存先のオブジェクトにスタブやモックなどを設定するやり方がある。そうすることで、依存先の内容やデータベースなどの外部リソースに依存せず、テスト対象のロジックの検証に集中できるようになる。

このように、CDIによる疎結合は、クラスごとのユニットテストを容易にする。

モックによるユニットテスト

SampleService が依存しているTeamDao をモックに差し替えることによって、ユニットテストを行う例を紹介する。

ここではモックを作成する方法として、Mockitoを使用する。 Mockitoは、Java のモックテストツールとして広く普及している代表的なライブラリである。

Mockitoの詳しい使い方は割愛するが、次のような有用な機能がある。

  • 継承・実装を行わずにモックを作成できる
  • メソッドの振る舞いや検証を柔軟に設定できる
  • インターフェースではなく既存のクラスからでもモックを作成できる

特に最後の機能は強力だ。

通常は、モックの差し替えを可能とするために、依存するオブジェクトをインターフェースと実装を分け、宣言にはインターフェースを指定することが多いだろう。

Mockitoを使えば、インターフェースと実装を分けなくてもモックを作ることができる。このため、テスト時にモックを使いたいというだけの理由であれば、全ての要素について、インターフェースと実装を分ける必要はなくなる。 DIを使用したことがある方であれば、際限なく増え続けるインターフェースとその実装クラス(xxxImpl) を見たことがあるのではないだろうか。

今回のサンプルでは、そのような理由でインターフェースと実装に分けていない。

テストクラスのサンプル

先ほどのSampleServiceallTeams メソッドについてのテストクラスの例を以下に記載する。(newTeamメソッドは後述する)。

public class SampleServiceTestByMock {
    
    @Mock // モックとして宣言し既存の振る舞いを変更する。
    TeamDao dao;
    
    @InjectMocks // モックを注入するオブジェクトの宣言
    SampleService target = new SampleService();
    
    @Before
    public void setUp() {
        // @InjectMockが付いているオブジェクトのフィールドに
        // @Mockが付与されたモックを注入する。
        MockitoAnnotations.initMocks(this);
        // モック daoのfindAllの振る舞いを変更。
        when(dao.findAll()).thenReturn(Arrays.asList(TeamOf("A", 1), TeamOf("B", 2)));
    }
    
    /**
     * DAOをモック化してテスト実施。
     */
    @Test
    public void testAllTeams() {
        List<Team> result = target.allTeams();
        assertThat(result.size(),  is(2));
        assertThat(result, is(Arrays.asList(TeamOf("A", 1), TeamOf("B", 2))));
    }

    // 便利メソッド
    private static Team TeamOf(String name, long version) {
        Team t = new Team();
        t.setName(name);
        t.setVersion(version);
        return t;
    }

}

TeamDao型の変数daoには、@Mockアノテーションを付与してある。このアノテーションは、TeamDaoインスタンスをモックとして振る舞いを変更可能にすることを指す。

テスト対象のSampleServiceインスタンスは、普通にコンストラクタで生成している。 (この方法でユニットテストを行う場合、 CDI周りのアノテーションなどは、実行に一切関係ない。)

setUp メソッドでは、MockitoAnnotations.initMocksメソッドを実行している。このメソッドによって、@InjectMocksアノテーションが付与されたSampleSeviceオブジェクト内のフィールドに対して、@Mockが設定されたTeamDaoのモックオブジェクトを注入している。 (これは、Mockitoのアノテーションによるモック設定機能を使用した例で、アノテーション以外にメソッド呼び出しでモックを設定する方法もある。)

setUpメソッドでは、変数daofindAllメソッドの戻り値を、2件のリストを返すように振る舞いを変更している。 これにより本来のメソッドは実行されなくなるため、TeamDaoの依存先のEntityMangerのことを考慮する必要はない。

testAllTeamsメソッドではallTeamsメソッドの戻り値を検証する。allTeamsメソッドは、TeamDaofindAllメソッドの結果を返すのみである。 事前にモックによって、2件のリストを返すようになっているので、結果は2件のリストのはずだ。

本来allTeamsメソッドは、データベースから内容を取得する処理を行うが、モックによってデータベースを利用せず、固定の値を取れるようになっている。allTeamsメソッドに分岐などがあれば、それに応じてテストパターンを増やせばよい。

CDIも含めたユニットテスト

開発作業がある程度進むと、モックを使ったテストだけでは物足りなくなり、データベースとの連携を確認したい場合も出てくるだろう。 CDIアノテーションの設定が正しく行われているかも確認したい場合があるかもしれない。

そのような場合、 CDIコンテナ(CDI 実行環境) を作成してJUnitを実行することが考えられる。

JUnitCDIコンテナを動かすツールには、以下のようなものがある。

今回は CDI-Unit を使ったの例を紹介する。理由はこのツールが、最も導入が簡単で機能が豊富だったためだ。

本来であれば、DeltaSpike のテスト機能を使用したかったのだが、設定が難しく、また Java EE7への対応が進んでいないためか動作するまでに至らなかった。

arquillian は実際にサーバーを稼動させるため、結合試験のようなより上位のテストを行うのに適している。このツールもいずれ解説したい。

CDI-Unitによるテストクラス

SampleServicenewTeamメソッドのテストを行うためのサンプルを掲載する。 このテストは、テスト用の組み込みデータベースを使用し、TeamDaoを通じてデータベースまでを連携させるテストを行う。

/**
 * CDIUnitによるDIを含めたテスト
 * 
 * 宣言的トランザクションはサポートされないので、
 * トランザクション制御は自前で行う。
 */
@RunWith(CdiRunner.class) // CDIUnitの動作で必須。
public class SampleServiceTestByCDI {
    
    @Inject // テスト対象 宣言のみ行う。
    SampleService target;
    
    // トランザクション制御で使用。targetの内部で使用するEntityMangerと共有。
    @Inject
    EntityManager em;
    
    EntityManagerFactory emf;
    
    // 初期処理
    @PostConstruct
    void init() {
      emf = Persistence.createEntityManagerFactory("ut");
    }
    
    /** UT用のEntityManagerの生成 */
    @Produces @ApplicationScoped
    EntityManager createUtEm() {
        System.out.println("called");
        return emf.createEntityManager();
    }
    
    /** トランザクションの開始 */
    @Before
    // このアノテーションにより、各テストクラスのメソッドや、
    // 対象オブジェクト内のインジェクション対象が共有される。
    @InRequestScope
    public void setUp() {
        em.getTransaction().begin();
        em.persist(TeamOf("A", 1));
        em.flush();
    }
    
    /** ロールバック */
    @After
    @InRequestScope
    public void tearDown() {
        em.getTransaction().rollback();
    }

    /** 1件登録を行うケース */
    @Test
    @InRequestScope
    public void testNewTeamOnNormal() throws Exception {
        
        target.newTeam("B");
        
        assertThat(em.find(Team.class, "B"), is(TeamOf("B", 1)));
    }
    
    /** チェック例外が起きるケース */
    @Test(expected = SampleException.class)
    @InRequestScope
    public void testCheckException() throws Exception {
        try {
            target.newTeam("checkng");
        } catch(SampleException e) {
            em.flush();
            assertThat(em.find(Team.class, "checkng"), 
                               is(TeamOf("checkng", 1)));
            throw e;
        }
    }
// 以下割愛

上のコードのポイントを簡単にまとめておく。

  • @RunWith(CdiRunner.class) によりCDI-UnitがCDI実行環境を生成してテストを行う。
  • テストクラスの中で、@Inject によるインジェクションが行える(テストクラス自身もCDI Beanである)。 テスト対象クラスの他、EntityManger のような他のCDIで管理されているオブジェクトも取得でき、プロパティの操作も行える。
  • インジェクションしたオブジェクトの生存期間は、各メソッドで宣言する@InRequestScope のようなアノテーションで決まる。SampleService はリクエストスコープのCDI Beanであるため、@InRequestScopeを宣言しておくことで、テストメソッドの単位で、CDI Beanのライフサイクルが設定される。
  • EntityManager はUT用の設定を使用するため、テストクラス内にProducerメソッドを作成している。
  • トランザクション管理は自前で行う。

テスト用データベースの設定のために、別途設定ファイルを作成している。 これらの詳細は、以前の記事githubのテストソースを参照いただきたい。

CDI-Unitの基本的な使い方を紹介した、その他の使用方法は公式サイトを参照いただきたい。

まとめ

CDI使用時のユニットテストの方法について記述した。

今回紹介したのは、CDI Beanのテスト方法だが、JSFJAX-RS, EJB なども、アノテーションを付与した単なるPOJOなので、今回と同じ方法でテストが可能だ。 Java EE はテストが難しいというのは、過去の話といってよいだろう。

[前多 賢太郎]