Sunday, 5 May 2013

Integration testing ReactiveMongo

During the development of Moresby Stroll I had to solve the testing of our data access layer. Moresby Stroll Server uses Akka Actors as a base of its architecture, therefore the data access layer is implemented by an Actor, the DataAccessActor.

DataAccessActor is responsible to provide all the persistence functionality what the business logic and higher layers require. Let's take a look at the DataAccessActor.

DataAccessActor

The DataAccessActor in the current state can process PlaceRequest messages and it will send back Place messages as answer.

def receive = {
    case PlaceRequest(coord, radius) =>
        println(s"Requesting places for $coord with $radius radius")

        import actors.dal.converters.PlaceConverter._

        val from = sender.path

        val x1 : Double = coord.lng - radius
        val y1 : Double = coord.lat - radius
        val x2 : Double = coord.lng + radius
        val y2 : Double = coord.lat + radius

        val cursor = collection.find(BSONDocument( "loc" -> BSONDocument(
        "$within" -> BSONDocument(      //GeoWithin doesn't work in tests.
          "$box" ->  BSONArray( BSONArray( x1, y1), BSONArray( x2, y2 ))
        )
        ))).cursor[Place]

        cursor.enumerate.apply(Iteratee.foreach(getResponseHandler(from)))
}

private[this] def getResponseHandler(fromPath : ActorPath) : (Place) => Unit = { place : Place => context.actorFor(fromPath) ! place }

The only trick what I had to apply here is that I had to use the sender.path and contex.actorFor() because the responseHandler function will run in different thread where the sender is no longer valid.

Testing

Because of Moresby Stroll Server is a Play 2 application and the default test framework in Play 2 is Specs2, we are using it to write our tests. Unit testing DataAccessActor is almost impossible, so I rather concentrated on the Integration testing. To build the enviornment for these integration tests I needed Actor system and Mongo database behind the test case.

Actor environment

Akka fortunately provides a TestKit which provides a test actor system, TestActors, etc. To integrate it to Specs2 I used Age Mooij's blog post: http://blog.xebia.com/2012/10/01/testing-akka-with-specs2/

MongoDB environment

MongoDB integration seems a bit more difficult, but fortunately it's also very easy. Aurélien Thieriot's Specs2-EmbedMongo project helps us out and with a trait it provides what we need.

Spec2-EmbedMongo uses Michael Mosmann's EmbedMongo tools what is able to download and start a mongoDB and you don't need to do anything special. The connection details will be localhost:12345.

DataAccessActorTest class

The DataAccessActorTest class contains the integration test of DataAccessActor. Let's take a closer look at this class:

class DataAccessActorTest extends Specification with EmbedConnection {

The class itself extends the Specs2 Specification and mixes in Aurélien's EmbedConnection trait. This trait will provide us MongoDB on localhost:12345.

The DataAccessActor picks up the connection parameters from application.conf. In the application.conf the mongodb properties are picked up from System properties. To use the test database we have to set these system properties:

System.setProperty("MONGODB_URL", "localhost:12345")
System.setProperty("MONGODB_USERNAME", "")
System.setProperty("MONGODB_PASSWORD", "")
System.setProperty("MONGODB_DB", "testDatabase")

Let's start writing the test itself. The normal Specs2 should-in structure is amended with the AkkaTestKitSpecs2Support from Age Mooij's blog, so the test actor context will be built up for the test not for the whole test case.

 "DataAccessActor" should {
   "find places if a user actor requests it" in new AkkaTestkitSpecs2Support {

In the test our first to-do is to create a test database. To upload our test data, firstly we have to connect ot it. Because ReactiveMongo is already used in the project I use it as well.

  /* Preparation */
  val driver     = new MongoDriver
  val connection = driver.connection("localhost:12345" :: Nil)
  val db         = connection("testDatabase")
  val collection = db("places")

The test data can be uploaded by the collection.insert() method. The test (a)waits for the resultm because we want to be sure that the record is inserted into the database.

  val insertResult = collection.insert(Place(BSONObjectID.generate.stringify, "name", Coordinate(1.1, 1.1)))
  Await.result(insertResult, DurationInt(15).seconds)

In our case, it is not enought to upload data because DataAccessActor uses the 2d search feature of MongoDB so the Geo2D index must be applied on the loc field.

  val idx : Index = Index( ("loc", Geo2D) :: Nil)
  Await.result(collection.indexesManager.ensure(idx) , DurationInt(15).seconds)

After all other preparation only the creation of the tested object left.

  /* Creating subject */
  val dalActor = system.actorOf(Props[DataAccessActor])

Finally, we can write the actual test:

  /* Test */
  dalActor ! PlaceRequest(Coordinate(1.1, 1.1), 1.0)

To assert the result I use the built in TestKit methods. Here you can see the use of expectMsgClass. I can't match directly the result object because of the generated id.

  /* Assertion */
  val result = expectMsgClass(DurationInt(30).seconds, classOf[Place])
  result.loc === Coordinate(1.1, 1.1)
  result.name === "name"

At the end of the test the created mongodb connection must be closed.

  /* Closing connection */
  println("closing initial connection")
  driver.close()
  connection.close()

Licence

The licence of Moreby Stroll Server is GPLv3 but the code above is under BSD 3-Clause licence. Fell free to use this code snippet in your commercial product.

  • Play - Apache 2
  • Specs2 - MIT like
  • Specs2-EmbedMongo - MIT
  • EmbedMongo - Apache 2

Disclosure

If you find any bug in the describtion or you can't understand it or you just have a question, please do not hesitate and comment it under the post or send me an email to the barnabas.sudy@gmail.com

4 comments:

  1. Thanks for the effort to help,

    My persistence logic uses ReactiveMongo but doesn't use Actor, so using your part to instantiate a Mongo connection using Embed Mongo and insert data, when I call my function it raises Exception in thread "Thread-21" java.net.SocketException: Connection reset

    The real problem here, is how to force ReactiveMongo Play plugin to connect to mongo at the test startup?

    I tried to use "in new WithApplication" but in vain, any advice?

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. I used this technique:

    "HandleQuery" should {
    val additionalConfiguration: Map[String, String] = Map("mongodb.db" -> "test-salamworld-influencer")

    "HandleQuery Should adhere client's query" in running(FakeApplication(additionalConfiguration=additionalConfiguration)) {

    This will start the ReactiveMongo plugin with my custom configuration

    ReplyDelete
  4. In the same spirit, Acolyte provides unit testing tool for code based on ReactiveMongo, so that during tests (e.g. with specs2) MongoDriver can be managed by query/write handler returning required result to represent various Mongo test cases: https://github.com/cchantep/acolyte/tree/master/reactive-mongo

    ReplyDelete