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
Thanks for the effort to help,
ReplyDeleteMy 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?
This comment has been removed by the author.
ReplyDeleteI used this technique:
ReplyDelete"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
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