ScalaでDIというかServiceLocator的な名状しがたい何か

ScalaでDIというかServiceLocator的な名状しがたい何かを簡単に実装してみた。
理由はテストを楽に書くため。LiftのSimpleInjectorも検討したが、まぁ、勉強がてら作成。
完成形のコードはgistに上げた
https://gist.github.com/2651257


例えばTwitterのクライアントを作るとする。
TwitterAPIにアクセスするようなクラスが

class TwitterApi {
  def publicTimeLines : List[String] = {
    //Twitter API つかってごにょごにょするはず
    List("本当は", "リアルに", "public", "timeline", "取得する")
  }

}

それのクライアントのコードが

class TwitterClient extends ApiInjector {

  def indexedPublicTimeLine : List[String] =  {
    val twitter = new TwitterApi
    twitter.publicTimeLines.zipWithIndex
      .map{ case (s, i) => (i + 1) + " " + s}
  }
}

と実装したとする


さぁ!TwitterClientのテストを書け!!!ちゃんと書け!
が、ダメ
実際にAPIにつなぎにいってるので、public time line は毎回変わるのでテストが書けない!!!


まずは、実装とインタフェースを分離する
Scalaでは Trait だね

trait TwitterApi {
  def publicTimeLines : List[String]
}

実装はこの trait を mixin しよう(traitが一個しかないときにはextends。複数ある場合は with)

class TwitterApiImpl extends TwitterApi {
  def publicTimeLines : List[String] = {
    //Twitter API つかってごにょごにょするはず
    List("本当は", "リアルに", "public", "timeline", "取得する")
  }
}


次に Injector な Trait を用意する

trait ApiInjector {
  var twitter : TwitterApi = new TwitterApiImpl;
}

クライアントのコードは Injector の Trait を mixin する

class TwitterClient extends ApiInjector {

  def indexedPublicTimeLine : List[String] =  {
    twitter.publicTimeLines.zipWithIndex
      .map{ case (s, i) => (i + 1) + " " + s}
  }
}

これでクライアントの動作は今までと変わらなくなった


さて、ではテストはどう書けばいい?


テスト用に、Apiを偽装してくれる trait を書く

trait TestApiInjector extends ApiInjector {
  twitter = new TwitterApi {
    def publicTimeLines = {
      List ("dummy", "public", "timeline")
    }
  }
}

テスト時には、TwitterClient にこの Trait を mixin して利用します。new TwitterClient with TestApiInjector がミソ

import org.specs2.mutable._

class TwitterClientTest extends Specification {

  "Twitter Api Client" should {
    val client = new TwitterClient with TestApiInjector
    val indexedPublicTimeLine = client.indexedPublicTimeLine
    "indexedPublicTimeLine return size " in {
      indexedPublicTimeLine.size must_== 3
    }

    "indexedPublicTimeLine return indexedLine" in {
      indexedPublicTimeLine(0) must_== "1 dummy"
    }

  }

}

ね。簡単でしょう?