LoginSignup
9
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-01-11

やりたいこと

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することで回避

テストサンプル

  • 実装
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)
}
  • サービスの単体テスト
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にできました。

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はインスタンス化して際にコネクションを取得していたので、このままだと一回使用した後は上記のエラーが発生します。

NamedDB.scala
private lazy val db: DB = DB(connectionPool().borrow())

とりあえずの回避方法として、DBオブジェクトのラッパーを作成しちゃいましたが。

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)
  }
}
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
}
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の責務も大分適当になっているので、良い方法がみつかったら改めて投稿しようと思います!!

9
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8