ScalikeJDBCのDBブロックを使ったクラスの単体テスト


## やりたいこと
ServiceクラスでScalikeJDBC(あとSkinnyOrmも)のDBブロックをMockっぽく処理して単体テストをしたい。

Playfamework上でやってるけど、それ以外でもできるはず


## 環境とか
* PlayFramework ver2.3.x
* SkinnyOrm ver1.3.6
* Scalikejdbc ver 2.2.0

## 実現方法
1. CakePatternを適当に使う

2. DBオブジェクトをNamedDBインスタンスに修正

3. DBSessionをMock

4. NamedDBをMock…
にしたかったけどDBブロックはテストの対象にしたい
上手くMockitoのanswersを使用すればいける気がするけど、overrideすることで回避

## テストサンプル
* 実装

```scala:Person.scala
case class Person(
  id: Long,
  name: String)

// リポジトリだよ
trait PersonRepositoryComponent {

  val personRepository: PersonRepository
  //ここは適当
  class PersonRepository
    extends SkinnyCRUDMapper[Person] {

    override val tableName = "people"
    override val defaultAlias = createAlias("p")

    override def extract(rs: WrappedResultSet, p: ResultName[Person]): Person = new Person(
      id = rs.get(p.id),
      name = rs.get(p.name))
  }

}

// サービスだよ
trait PersonServiceComponent {
  this: PersonRepositoryComponent =>

  val personService: PersonService
  val db: NamedDB

  class PersonService {
    def create(name: String): Person = {
      val id = db.localTx { implicit session =>
        // このブロックをテスト対象にする
        personRepository.createWithAttributes('name -> name)
      }
      Person(id, name)
    }
  }
}

// レジストリ(singleton)だよ
object PersonRegister extends PersonRepositoryComponent with PersonServiceComponent {
  val personRepository = new PersonRepository
  val personService = new PersonService
  val db = NamedDB(ConnectionPool.DEFAULT_NAME)
}
```

* サービスの単体テスト
 
```scala:PersonSpec.scala
package models

import org.mockito.Matchers._
import org.mockito.Mockito._
import org.specs2.mutable.Specification
import scalikejdbc._

// いろいろ不都合があったのでMockitoをミックスインしない
// 直接Mockitoを使うよ
class PersonSpec extends Specification {

  object Register
    extends PersonServiceComponent
    with PersonRepositoryComponent {
    val personRepository = mock(classOf[PersonRepository])
    val personService = new PersonService

    val db: NamedDB = new NamedDB(ConnectionPool.DEFAULT_NAME) {
    
      # @todo Stubでできると思うけど挫折
      override def localTx[A](execution: DBSession => A)
                              (implicit boundary: TxBoundary[A]): A = {
        execution.apply(mock(classOf[DBSession]))
      }
    }

  }

  "Person" should {
    "#create" in {
      val expectedPerson = Person(1, "person_name")
      when(Register.personRepository.createWithAttributes(any[(Symbol, Any)])(any[DBSession])) thenReturn expectedPerson.id
      val actualPerson = Register.personService.create(expectedPerson.name)
      actualPerson mustEqual expectedPerson
    }
  }
}

```


## そもそも
* Mockitoで、引数の関数処理をそのまま実行するようにすれば、もう少しスマートに解決できるけど、途中で挫折。
Mockito難しい、だれか教えて！！

* SkinnyOrmの使い方は正しいのだろうか。

## 追記
### 2015-01-16 01
NamedDBもanswerでmockにできました。


```scala:PersonSpec.scala
class PersonSpec extends Specification {

  // テストごとに変数を初期化
  isolated

  val r = new PersonServiceComponent
    with PersonRepositoryComponent {
    val personRepository = mock(classOf[PersonRepository])
    val personService = new PersonService
    val db = mock(classOf[NamedDB])
  }

  "Person" should {
    "#create" in {
      val expectedPerson = Person(1, "person_name")
      when(r.personRepository.createWithAttributes(any[(Symbol, Any)])(any[DBSession])) thenReturn expectedPerson.id
      when(r.db.localTx(any()))
        .thenAnswer(new Answer[Any] {
        def answer(p1: InvocationOnMock): Any = {
          p1.getArguments()(0).asInstanceOf[DBSession => Any].apply(mock(classOf[DBSession]))
        }
      })

      val actualPerson = r.personService.create(expectedPerson.name)
      verify(r.personRepository, times(1)).createWithAttributes(any[(Symbol, Any)])(any[DBSession])
      verify(r.db, times(1)).localTx(any())
      actualPerson mustEqual expectedPerson
    }
  }
}
```

### 2015-01-16 02
テストは通るけど、実動作では「java.sql.SQLException: Connection is closed」が発生していました…
NamedDBはインスタンス化して際にコネクションを取得していたので、このままだと一回使用した後は上記のエラーが発生します。

```scala:NamedDB.scala
private lazy val db: DB = DB(connectionPool().borrow())
```
とりあえずの回避方法として、DBオブジェクトのラッパーを作成しちゃいましたが。

```scala:DBWrapperComponent.scala
import scalikejdbc.{ DB, DBSession }

trait DBWrapperComponent {
  val db: DBWrapper

  class DBWrapper {
    def localTx[A](execution: DBSession => A): A = DB.localTx(execution)
    def readOnly[A](execution: DBSession => A): A = DB.readOnly(execution)
  }
}
```
```scala:Person.scala
import models.repository.DBWrapperComponent
import scalikejdbc.{WrappedResultSet, _}
import skinny.orm.SkinnyCRUDMapper

case class Person(
  id: Long,
  name: String)

// リポジトリだよ
trait PersonRepositoryComponent {

  val personRepository: PersonRepository
  //ここは適当
  class PersonRepository
      extends SkinnyCRUDMapper[Person] {

    override val tableName = "people"
    override val defaultAlias = createAlias("p")

    override def extract(rs: WrappedResultSet, p: ResultName[Person]): Person = new Person(
      id = rs.get(p.id),
      name = rs.get(p.name))
  }

}

// サービスだよ
trait PersonServiceComponent {
  this: PersonRepositoryComponent with DBWrapperComponent =>

  val personService: PersonService

  class PersonService {
    def create(name: String): Person = {
      val id = db.localTx { implicit session =>
        // このブロックをテスト対象にする
        personRepository.createWithAttributes('name -> name)
      }
      Person(id, name)
    }
  }
}

// レジストリ(singleton)だよ
object PersonRegister
    extends PersonRepositoryComponent
    with PersonServiceComponent
    with DBWrapperComponent {
  val personRepository = new PersonRepository
  val personService = new PersonService
  val db = new DBWrapper
}
```

```scala:PersonSpec.scala
import models.repository.DBWrapperComponent
import org.mockito.Matchers._
import org.mockito.Mockito._
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import org.specs2.mutable._
import scalikejdbc.DBSession

// テストだよ
class PersonSpec extends Specification {

  // テストごとに変数を初期化
  isolated

  val r = new PersonServiceComponent with PersonRepositoryComponent with DBWrapperComponent {
    val personRepository = mock(classOf[PersonRepository])
    val personService = new PersonService
    val db = mock(classOf[DBWrapper])
  }

  "Person" should {
    "#create" in {
      val expectedPerson = Person(1, "person_name")
      when(r.personRepository.createWithAttributes(any[(Symbol, Any)])(any[DBSession])) thenReturn expectedPerson.id
      when(r.db.localTx(any()))
        .thenAnswer(new Answer[Any] {
          def answer(p1: InvocationOnMock): Any = {
            p1.getArguments()(0).asInstanceOf[DBSession => Any].apply(mock(classOf[DBSession]))
          }
        })

      val actualPerson = r.personService.create(expectedPerson.name)
      verify(r.personRepository, times(1)).createWithAttributes(any[(Symbol, Any)])(any[DBSession])
      verify(r.db, times(1)).localTx(any())
      actualPerson mustEqual expectedPerson
    }
  }
}

```

今回は上記で逃げましたが、作製者の方から助言を頂いた通り、もう一枚サービスを挟んでモック化するのがベターだと思います。
ということで、RepositoryとかServiceの責務も大分適当になっているので、良い方法がみつかったら改めて投稿しようと思います！！