Apache Camel - APIコンポーネントのテスト

f:id:tasato-redhat:20190314152401p:plain

Red Hatの佐藤匡剛です。

APIコンポーネントフレームワーク紹介記事の第4回目(最終回)です。前回は、フレームワークの中核であるAPIのマッピング設定を詳しく見ました。今回はプロジェクトを完成させるためのインテグレーションテストについて説明します。

コンポーネントのインテグレーションテスト

Camelコンポーネントは外部サービスとの連携を提供するコードですので、それをきちんとテストするとなると必然的にインテグレーションテストになります。したがって、インテグレーションテストを追加するまではプロジェクトが完成したとは言えません。

APIコンポーネントフレームワークは、インテグレーションテストまで自動で用意してくれています。第2回のところでビルド後に生成されるソースコードについて眺めましたが、そこでチラッと出てきたテストコードの出番です。

プロジェクトにインテグレーションテストを追加する

もう一度camel-hello-component/target/generated-test-sources/camel-component/を見てみます。

camel-hello-component/target/
└── generated-test-sources/camel-component/
    └── com/redhat/sample/camel/hello/
        ├── HelloFileHelloIntegrationTest.java
        └── HelloJavadocHelloIntegrationTest.java

見て分かるように、APIクラス毎にインテグレーションテストが作成されています。camel-helloコンポーネントの例では、hello-filehello-javadocに対してそれぞれxxxxxIntegrationTest.javaが生成されています。

試しにHelloFileHelloIntegrationTest.javaを開いて見ましょう。コメントに指示が書いてあります。

/**
 * Test class for {@link com.redhat.sample.camel.hello.api.HelloFileHello} APIs.
 * TODO Move the file to src/test/java, populate parameter values, and remove @Ignore annotations.
 * The class source won't be generated again if the generator MOJO finds it under src/test/java.
 */
public class HelloFileHelloIntegrationTest extends AbstractHelloTestSupport {
   [...]

このテストクラスをsrc/test/javaに移して、パラメータの値をセットし、@Ignoreを外せ、という指示です。また、ソースがsrc/test/javaに移されると、次のビルドからは同じクラスは自動生成されないとも書いてあります。

指示通りに、2つのテストクラスをcamel-hello-component/src/test/javaに移します。

インテグレーションテストを完成させる

src/test/javaにテストを移したら、改めてテストクラスの中身を見てみます。

public class HelloFileHelloIntegrationTest extends AbstractHelloTestSupport {

    private static final Logger LOG = LoggerFactory.getLogger(HelloFileHelloIntegrationTest.class);
    private static final String PATH_PREFIX = HelloApiCollection.getCollection().getApiName(HelloFileHelloApiMethod.class).getName();

    // TODO provide parameter values for greetMe
    @Ignore
    @Test
    public void testGreetMe() throws Exception {
        // using String message body for single parameter "name"
        final String result = requestBody("direct://GREETME", null);

        assertNotNull("greetMe result", result);
        LOG.debug("greetMe: " + result);
    }

    // TODO provide parameter values for greetUs
    @Ignore
    @Test
    public void testGreetUs() throws Exception {
        final Map<String, Object> headers = new HashMap<String, Object>();
        // parameter type is String
        headers.put("CamelHello.name1", null);
        // parameter type is String
        headers.put("CamelHello.name2", null);

        final String result = requestBodyAndHeaders("direct://GREETUS", null, headers);

        assertNotNull("greetUs result", result);
        LOG.debug("greetUs: " + result);
    }

    @Ignore
    @Test
    public void testSayHi() throws Exception {
        final String result = requestBody("direct://SAYHI", null);

        assertNotNull("sayHi result", result);
        LOG.debug("sayHi: " + result);
    }

    @Override
    protected RouteBuilder createRouteBuilder() throws Exception {
        return new RouteBuilder() {
            public void configure() {
                // test route for greetMe
                from("direct://GREETME")
                    .to("hello://" + PATH_PREFIX + "/greetMe?inBody=name");

                // test route for greetUs
                from("direct://GREETUS")
                    .to("hello://" + PATH_PREFIX + "/greetUs");

                // test route for sayHi
                from("direct://SAYHI")
                    .to("hello://" + PATH_PREFIX + "/sayHi");

            }
        };
    }
}

AbstractHelloTestSupportは元々src/test/javaに生成されているインテグレーションテストの抽象ベースクラスです。すべてのテストクラスに共通の初期化処理やヘルパーメソッドを提供したい場合は、このベースクラスを使います。

とりあえず冒頭コメントの指示通り、@Ignoreを外してテストパラメータを適宜セットしましょう。

    @Test
    public void testGreetUs() throws Exception {
        final Map<String, Object> headers = new HashMap<String, Object>();
        // parameter type is String
        headers.put("CamelHello.name1", "Llama");
        // parameter type is String
        headers.put("CamelHello.name2", "Alpaca");

        final String result = requestBodyAndHeaders("direct://GREETUS", null, headers);

        assertNotNull("greetUs result", result);
        LOG.debug("greetUs: " + result);
    }

これでひとまずmvn testがパスするようになるはずです。

TIPS: 標準APIコンポーネントの使い方を知るには

少し脱線しますが、Camel標準のAPIコンポーネントの使い方を知りたいときにも、この自動生成されたテストケースが役に立ちます。Camelのコンポーネントはドキュメントがよく整備されていますが、それでも実際に使おうとすると、どのエンドポイントにどのオプションが必要かといった情報が足りないことがあります。

その場合、APIコンポーネントフレームワークを使ったコンポーネントであれば、このフレームワークから自動生成されたテストを見ることで、エンドポイント毎にどのオプションが最低限必要なのかが一目で分かります。例えば、camel-boxのbox://files/downloadFileエンドポイントのテストは以下の通りです。

https://github.com/apache/camel/blob/camel-2.23.1/components/camel-box/camel-box-component/src/test/java/org/apache/camel/component/box/BoxFilesManagerIntegrationTest.java

    @Test
    public void testDownloadFile() throws Exception {
        final Map<String, Object> headers = new HashMap<>();
        // parameter type is String
        headers.put("CamelBox.fileId", testFile.getID());
        // parameter type is java.io.OutputStream
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        headers.put("CamelBox.output", output);
        // parameter type is Long
        headers.put("CamelBox.rangeStart", null);
        // parameter type is Long
        headers.put("CamelBox.rangeEnd", null);
        // parameter type is com.box.sdk.ProgressListener
        headers.put("CamelBox.listener", null);

        final java.io.OutputStream result = requestBodyAndHeaders("direct://DOWNLOADFILE", null, headers);

        assertNotNull("downloadFile result", result);
        LOG.debug("downloadFile: " + result);
    }

fileIdoutputが最低限必要なのが分かるでしょうか。

テストケースに認証情報を渡す

後はAPIライブラリを本物のWebサービスのものに差し替え、ちゃんとしたテストケースをどんどん追加していくだけなのですが、そのために必要な最後のポイントはサービスの認証情報をどうやってテストケースに渡すか、です。

インテグレーションテストのベースクラスAbstractHelloTestSupportを改めて見てみます。

public class AbstractHelloTestSupport extends CamelTestSupport {

    private static final String TEST_OPTIONS_PROPERTIES = "/test-options.properties";

    @Override
    protected CamelContext createCamelContext() throws Exception {

        final CamelContext context = super.createCamelContext();

        // read Hello component configuration from TEST_OPTIONS_PROPERTIES
        final Properties properties = new Properties();
        try {
            properties.load(getClass().getResourceAsStream(TEST_OPTIONS_PROPERTIES));
        } catch (Exception e) {
            throw new IOException(String.format("%s could not be loaded: %s", TEST_OPTIONS_PROPERTIES, e.getMessage()),
                e);
        }

        Map<String, Object> options = new HashMap<String, Object>();
        for (Map.Entry<Object, Object> entry : properties.entrySet()) {
            options.put(entry.getKey().toString(), entry.getValue());
        }

        final HelloConfiguration configuration = new HelloConfiguration();
        IntrospectionSupport.setProperties(configuration, options);

        // add HelloComponent to Camel context
        final HelloComponent component = new HelloComponent(context);
        component.setConfiguration(configuration);
        context.addComponent("hello", component);

        return context;
    }

テストのブート時に呼ばれるcreateCamelContext()メソッドを見ると、test-options.propertiesファイルからプロパティを読み込み、それをプロジェクトのxxxxxConfigurationクラスに適用しているのが分かります。test-options.propertiesファイルは、camel-hello-component/src/test/resources/test-options.propertiesに見つかります。

つまり、実際のWebサービスとのインテグレーションテストに必要な認証情報などは、このtest-options.propertiesファイルにセットすればいいのです。(認証情報なので、ソースコードリポジトリにはコミットしないように注意しましょう。)

例えば、camel-google-driveのtest-options.propertiesは以下の通りです。

https://github.com/apache/camel/blob/camel-2.23.1/components/camel-google-drive/src/test/resources/test-options.properties

#####################################
## Login properties for Google Drive Component
#####################################
## Application client id and secret
clientId=
clientSecret=
applicationName=camel-google-drive/1.0
#accessToken=
refreshToken=

camel-google-driveコンポーネントをテストしたい開発者は、この空のプロパティに適切に自分のテスト用の認証情報をセットしてインテグレーションテストを実行します。

おわりに

以上、全4回でAPIコンポーネントフレームワークの使い方を一通り説明しました。実際にCamelコンポーネントを開発しようとすると、まだまだ知っておくべき知識が色々と出てくると思いますが、それはコンポーネント開発一般に必要な知識なので、また別の機会に解説したいと思います。

とりあえず、分からないことがあったらまずはCamelの標準コンポーネントのソースコードを眺めてみる、というのが次のステップです(Camelに限らずOSS一般に言えることですが)。

今回作成したcamel-helloコンポーネントのソースコードは、以下のGitHubリポジトリに上がっています。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。