4
votes

The documentation for unit testing a Scala application https://www.playframework.com/documentation/2.4.x/ScalaTestingWithScalaTest talks about mocking the database access using Mockito. While this method works very well to test methods that get information from the database, I'm not seeing a clear solution how to test methods that insert, update or delete data.

This is what I have setup so far:

trait UserRepository { self: HasDatabaseConfig[JdbcProfile] =>
  import driver.api._

  class UserTable(tag: Tag) extends Table[userModel](tag, "users") {
     def id = column[Int]("id", O.PrimaryKey, O.AutoInc )
     def email = column[String]("email")
     def * = (id.?, email) <> (userModel.tupled, userModel.unapply _)
  }

  def allUsers() : Future[Seq[userModel]]
  def update(user: userModel) : Future[Int]
}

class SlickUserRepository extends UserRepository with HasDatabaseConfig[JdbcProfile] {
  import driver.api._
  protected val dbConfig = DatabaseConfigProvider.get[JdbcProfile](Play.current)

  private val users = TableQuery[UserTable]

  override def allUsers(): Future[Seq[userModel]] = {
     db.run(users.result)
  }

  def update(user: userModel): Future[Int] = {
     db.run(userTableQuery.filter(_.id === user.id).update(user))          
  }
}

class UserService(userRepository: UserRepository) {
  def getUserById(id: Int): Future[Option[userModel]] = {
     userRepository.allUsers().map { users =>
        users.find(_.id.get == id)
  }

  // TODO, test this...
  def updateUser(user: userModel): Future[Int] = {
     userRepository.update(user)
  }
}

And then my tests:

class UserSpec extends PlaySpec with MockitoSugar with ScalaFutures {
  "UserService" should {
    val userRepository = mock[UserRepository]
    val user1 = userModel(Option(1), "[email protected]")
    val user2 = userModel(Option(2), "[email protected]")

    // mock the access and return our own results
    when(userRepository.allUsers) thenReturn Future {Seq(user1, user2)}

    val userService = new UserService(userRepository)

    "should find users correctly by id" in {
      val future = userService.getUserById(1)

      whenReady(future) { user =>
        user.get mustBe user1
      }
    }

    "should update user correctly" in {
       // TODO test this
    }
}

I suppose I need to mock out the 'update' method and create a stub that takes the argument and updates the mocked data. However, my skills in Scala are limited and I can't wrap my head around it. Is there perhaps a better way?

Thanks!

1
As Slick is based on JDBC, you can use Acolyte framework to define simulated/mock connection for tests. - cchantep

1 Answers

1
votes

I'd recommend two unit test classes here. One for testing the logic in the UserService class. Another test class that tests the logic the UserRepository class (for this one use a dummy test class that extends the trait). Since the SlickUserRepository class has its own test coverage this allows the UserService test class to use a mock[UserRepository] in its own tests without degrading coverage and its tests only focus on its class' logic.

Doing this really simplifies the UserService tests so I won't dwell on those.

For the SlickUserRepository tests, I would recommend restructuring the logic in the SlickUserRepository class.

I'd recommend separating the logic living inside the db.run and have it as a separate method that constructs an action. This allows you to write direct tests on that logic that lives inside the "db.run{}".

You will find having the db.run integrated inside your update method as it is now will impair your ability to construct transactions that encompass multiple table calls. DbActions need to be chained together and run in one db.run(myDbAction.transactionally) to be transactional. Thats why I personally have my db.run logic in a business logic layer and not directly inside the persistence layer like it is in your example.

where ever you do have a db.run call its nice to place it as a separate method so you can easily spy around the call:

def run[M](action: DBIO[M]): Future[M] = {
  db.run(action)
}

futures don't need mocked. Simply defined these as result you want:

Future.failed("your intanciated exception")
Future.success("your intanciated success class")