ApacheCamel(camel-scala)でRouteBuilderのテストを実施する


# ビルドファイル
scalatestは必須ではないが、Matchersを使いたかったので追加している。
``` assertThat("result", is("expected")``` ではなく、 ```"result" should be("expected")``` みたいに評価したかった。

``` build.sbt
libraryDependencies ++= Seq(
  "org.scalatest" % "scalatest_2.11" % "2.2.4" % "test",
  "org.apache.camel" % "camel-testng" % "2.15.0" % "test"
)
```

``` pom.xml
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-test</artifactId>
      <version>2.15.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.scalatest</groupId>
      <artifactId>scalatest_2.11</artifactId>
      <version>2.2.4</version>
      <scope>test</scope>
    </dependency>
```

# Sample

恐らくこのサンプルコードを見れば、やりたいことのほとんどが解決出来てしまうかも。

サンプルはせっかくなので、以下の点を工夫してみた。

1. propertiesファイルを読み込んでテスト。
1. Exchangeの値を評価出来るようにする。(詳細は後述のTips参照)
  - camel-testが提供する既存の```org.apache.camel.test.junit4.CamelTestSupport```では、Route通過後のExchangeの状態を取得出来ないため評価が出来ない。

これで一気にテストの幅が広がるはず。
(RouteBuilder内のMock化や分岐については、明確な答えが見つかってないので後述のTipsで紹介。)

```PropertiesRouteBuilderTest.scala
package jp.den3umegumi.experimental.camel.route

import org.apache.camel.CamelContext
import org.apache.camel.builder.RouteBuilder
import org.apache.camel.component.properties.PropertiesComponent
import org.apache.camel.impl.DefaultCamelContext
import org.apache.camel.scala.dsl.builder.{RouteBuilderSupport, ScalaRouteBuilder}
import org.junit.Test

class PropertiesRouteBuilderTest  extends CamelTestSupportExtension with RouteBuilderSupport {

  override def createRouteBuilder: RouteBuilder = new ScalaRouteBuilder(context = new DefaultCamelContext) {
    "direct:property-read-route" ==> {
      setHeader("h_foo", simple("properties:foo"))
      // javaDSL には BuilderSupport#simple(String value, Class<?> resultType)があり
      // 戻り値を指定できるがscalaは不可
      setHeader("h_num", simple("properties:num"))
      setHeader("h_num", _.in("h_num").asInstanceOf[String].toInt)
    }
  }

  override protected def createCamelContext: CamelContext = {
    val context = super.createCamelContext()
    context.addComponent("properties", new PropertiesComponent("classpath:jp/den3umegumi/experimental/camel/route/PropertiesRouteBuilderTest.properties"))
    context
  }

  @Test
  def propertyRead = {
    // 下記のTipsを参照
    val in = sendBodyWithReceiveExchange("direct:property-read-route", "").getIn
    in.getHeader("h_foo") should be("bar")
    in.getHeader("h_num") should be(1234)
  }
}
```

## def createRouteBuilder: RouteBuilder

テスト対象のRouteBuilderを生成する関数。
今回はテストのRouteBuilderをテストクラス内で定義&生成。
こうすることで実験コードと動作コードが1クラス内で簡単に記述出来る。
恐らく通常の使い方は作成したRouteBuilderをnewする。

```scala
  override def createRouteBuilder: RouteBuilder = new ScalaRouteBuilder(context = new DefaultCamelContext) {
    "direct:property-read-route" ==> {
      setHeader("h_foo", simple("properties:foo"))
      // java には BuilderSupport#simple(String value, Class<?> resultType)があり
      // 戻り値を指定できるがscalaは不可
      setHeader("h_num", simple("properties:num"))
      setHeader("h_num", _.in("h_num").asInstanceOf[String].toInt)
    }
  }
```


# propertyファイル読み込み
createCamelContext をoverrirdeして生成したCamelContextにPropertyComponentをaddする。

```scala
  override protected def createCamelContext: CamelContext = {
    val context = super.createCamelContext()
    context.addComponent("properties", new PropertiesComponent("classpath:jp/den3umegumi/experimental/camel/route/PropertiesRouteBuilderTest.properties"))
    context
  }
```


# Tips

## RouteBuilder通過後のExchangeの値を評価する

サンプルではBodyしか送信出来ないが、必要に応じてHeaderを送信する拡張Methodも作成する。

```CamelTestSupportExtension.scala
package jp.den3umegumi.experimental.camel.route

import org.apache.camel.test.junit4.CamelTestSupport
import org.apache.camel.{Exchange, Processor}
import org.scalatest.Matchers

class CamelTestSupportExtension extends CamelTestSupport with Matchers {

  def sendBodyWithReceiveExchange(endpointUri: String, body: Any): Exchange = template.send(endpointUri, new Processor {
    def process(exchange: Exchange) {
      val in = exchange.getIn
      in.setBody(body)
    }
  })
}
```



## mock化
現在のcamelではRouteの一部をmock化することは出来ない。
そのためDBアクセスや外部通信を行う箇所などは関数切り出しをして、対象クラスを継承したクラスを定義するのが良さそう。
弱点は1つのテストクラス内に複数のケースが記述しづらい。

1. overrideした関数の中でテストケースで設定されたHeaderを判断する。(Propertyファイルを使ったり)
1. 1クラス1ケースで記述する。

というような対策が考えられるけど、どっちも可読性悪そう。


例

```MockRouteBuilder.scala
package jp.den3umegumi.experimental.camel.route

import org.apache.camel.CamelContext
import org.apache.camel.impl.DefaultCamelContext
import org.apache.camel.scala.dsl.builder.ScalaRouteBuilder

class MockRouteBuilder(context: CamelContext = new DefaultCamelContext) extends ScalaRouteBuilder(context: CamelContext) {

  private[route] def callAPI = {
    // なんか外部にアクセスしたり
    -->("mock:callAPI")
    setHeader("result", "foo")
  }

  "direct:mockroutebuilder" ==> {
    callAPI
    choice {
      when (_.in("result") == "foo") -->("mock:foo")
      otherwise -->("mock:not-foo")
    }
    -->("mock:mockroutebuilder-end")
  }
}
```

```MockRouteBuilderTest.scala
package jp.den3umegumi.experimental.camel.route

import org.apache.camel.builder.RouteBuilder
import org.apache.camel.impl.DefaultCamelContext
import org.apache.camel.scala.dsl.builder.RouteBuilderSupport
import org.apache.camel.test.junit4.CamelTestSupport
import org.junit.Test
import org.scalatest.Matchers

class MockRouteBuilderTest extends CamelTestSupport with Matchers with RouteBuilderSupport {

  override def createRouteBuilder: RouteBuilder = new MockRouteBuilder(context = new DefaultCamelContext) {
    override def callAPI = {
      -->("mock:mock-callAPI")
      setHeader("result", "bar")
    }
  }

  @Test
  def resultNotFoo = {
    getMockEndpoint("mock:callAPI").expectedMessageCount(0)
    getMockEndpoint("mock:mock-callAPI").expectedMessageCount(1)
    getMockEndpoint("mock:foo").expectedMessageCount(0)
    getMockEndpoint("mock:not-foo").expectedMessageCount(1)
    template.sendBody("direct:mockroutebuilder", "")
    assertMockEndpointsSatisfied
  }
}
```