Testing Play + Slick app
Asked Answered
A

2

7

I've a simple CRUD app built with Scala Play 2.4.3 and Play-slick 1.1.0 (slick 3.1.0) that uses a MySQL database for persistent storage.

I was trying to create the tests for my app and I saw 2 main options:

  • mocking database access, that as far as I've seen, requires some code changes
  • make tests use an alternative database (probably, in memory H2).

What's the best approach (vantages and desavantages)?

I prefer the second approach, but I'm finding some difficulties in setting up the tests.

What do I need to do? First, I think that I need to do the tests run with a FakeApplication, right? Do I need any sbt dependency to be able to do that?

After that, how do I specify to use the H2 database?

Auditor answered 29/11, 2015 at 22:10 Comment(0)
A
5

my solution was to add step(Play.start(fakeApp)) in the beginning of each spec, and step(Play.stop(fakeApp)) in the end of each spec.

Also:

def fakeApp: FakeApplication = {
FakeApplication(additionalConfiguration =
  Map(
    "slick.dbs.default.driver" -> "slick.driver.H2Driver$",
    "slick.dbs.default.db.driver" -> "org.h2.Driver",
    "slick.dbs.default.db.url" -> "jdbc:h2:mem:play"
  ))

}

This was needed because I'm using play-slick, which requires configurations like:

slick.dbs.default.driver = "slick.driver.MySQLDriver$"
slick.dbs.default.db.driver = "com.mysql.jdbc.Driver"
slick.dbs.default.db.url = "jdbc:mysql://localhost/database"
slick.dbs.default.db.user = "user"
slick.dbs.default.db.password = "password"

more info on the docs

Auditor answered 6/12, 2015 at 23:41 Comment(2)
What is this step? The keyword is not easy to search for.Rhodonite
Most likely the function from specs2, which allows you to run certain things before and/or after tests. Similar to what you would use a beforeAll / afterAll for.Wallenstein
O
6

I had the same struggle and I came up with a solution like this(using second approach):

Create a context for DAO to use:

trait BaseContext{

  def dbName: String

  val dbConfig = DatabaseConfigProvider.get[JdbcProfile](dbName)
  val db = dbConfig.db
  val profile = dbConfig.driver
  val tables = new Tables {  // this is generated by Schema Code Generator
    override val profile: JdbcProfile = dbConfig.driver
  }
}

@Singleton
class AppContext extends BaseContext{
  def dbName = "mysql"  // name in your conf right after "slick.dbs"
}

@Singleton
class TestingContext extends BaseContext{
  def dbName = "h2"
}

Then create a module to bind the injection, and don't forget to enable it in conf using play.modules.enabled += "your.Module":

class ContextModule(environment: Environment, configuration: Configuration) extends AbstractModule {

  override def configure(): Unit = {
    if (configuration.getString("app.mode").contains("test")) {
      bind(classOf[BaseContext])
          .to(classOf[TestingContext])
    } else {
      bind(classOf[BaseContext])
          .to(classOf[AppContext])
    }
  }
}

And inject it to every DAO you've created:

class SomeDAO @Inject()(context: BaseContext){

  val dbConfig = context.dbConfig
  val db = context.db
  val tables = context.tables
  import tables.profile.api._

  def otherStuff....
  // you can call db.run(...), tables.WhateverYourTableIs, tables.TableRowCaseClass, ...
}

And final step, your configuration file. In my case I used app.mode to mark the environment, and I use separate .conf for different environment. Of cause, in these conf you must have the correct DB configuration. Here's the sample:

app.mode = "test"

# Database configuration
slick.dbs = {
  # for unit test
  h2 {
    driver = "slick.driver.H2Driver$"
    db = {
      url = "jdbc:h2:mem:test;MODE=MYSQL"
      driver = "org.h2.Driver"
      keepAliveConnection = true
    }
  }
}

I'm pretty sure my solution is not a elegant one, but it deliver the goods. :) Any better solution is welcomed!

Oeflein answered 3/12, 2015 at 2:37 Comment(7)
Turns out Typesafe has a project demonstrating exactly this. github.com/typesafehub/activator-slick-multidbLimiter
I'm fully aware of that example and it did give me some hints. However, that didn't meet my expectation 100%.Oeflein
What were you missing? In the latest branch, 3.1, they've what you show above and then some.Limiter
Take that demo project for example, it demonstrates how slick access 2 DB at runtime, while my main goal is to replace mysql with h2 in tests, with SAME business logic (one set of code). That means I don't have to care about DB whatsoever while coding for actual feature or unit tests. That makes conf file the best place, so I code my way out to it. After that I'm fully content and did not track the newest slick. If there is a better way of doing so, please enlighten me. It really bothered me for a long time.Oeflein
I'm figuring this out myself. They don't seem to provide a good way to do this. I start an in-memory H2 in tests and have 2 DAO, one using Slick ORM, another plain SQL. There are various settings that seem to randomly affect the tests, like whether or not connection pool is enabled, the PatienceConfig (from org.scalatest.concurrent). I create and delete a test user before and after every test and sometimes the user is getting deleted before the test finishes. If I run within a transaction, the results are different. None of this happens with MongoDB, only RDBMS (in-memory?)Limiter
Feel free to check out my project if you're interested. github.com/abhijitsarkar/akka/tree/master/user-serviceLimiter
Yeah, that can be pain as hell. After a long period of debugging and questioning life, I've fixed my mind to fit this concurrent world. I started to realize that there were multiple h2 instances living inside those test threads and I could not assume order of them. That is rare because the real-life DB is considered as "single" truth.Oeflein
A
5

my solution was to add step(Play.start(fakeApp)) in the beginning of each spec, and step(Play.stop(fakeApp)) in the end of each spec.

Also:

def fakeApp: FakeApplication = {
FakeApplication(additionalConfiguration =
  Map(
    "slick.dbs.default.driver" -> "slick.driver.H2Driver$",
    "slick.dbs.default.db.driver" -> "org.h2.Driver",
    "slick.dbs.default.db.url" -> "jdbc:h2:mem:play"
  ))

}

This was needed because I'm using play-slick, which requires configurations like:

slick.dbs.default.driver = "slick.driver.MySQLDriver$"
slick.dbs.default.db.driver = "com.mysql.jdbc.Driver"
slick.dbs.default.db.url = "jdbc:mysql://localhost/database"
slick.dbs.default.db.user = "user"
slick.dbs.default.db.password = "password"

more info on the docs

Auditor answered 6/12, 2015 at 23:41 Comment(2)
What is this step? The keyword is not easy to search for.Rhodonite
Most likely the function from specs2, which allows you to run certain things before and/or after tests. Similar to what you would use a beforeAll / afterAll for.Wallenstein

© 2022 - 2024 — McMap. All rights reserved.