Dependency Injection in Scala and Play! using the cake pattern

It is my first time using Scala and I wanted a simple way to do DI so I could unit test my controller easily. The ability to use traits really seems to have a positive impact on code design, and enables things like DI to be quite easy without a runtime framework.

Real work scala dependency injection was a great introduction to implementing the Cake pattern in Scala so I won’t repeat the specifics of it here.

In case you haven't come across it the Play! framework is a complete web framework for JVM languages.

Personally I did not want to override plays controller instantiation so things stayed simple and I kept a clean routes table.

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

POST    /:topic/:key          controllers.Application.send(topic: String, key: String)
GET     /:topic/:key          controllers.Application.get(topic: String, key: String, groupId: String)

The standard play pattern changes slightly with the controller which is split between two definitions. The first is the Application object which works with the play route and inherites the ApplicationController trait. It defines a message consumer to a real implementation which is going to connect to a Kafka cluster and it is this that we will want to mock when running our unit tests.

object Application extends ApplicationController {
  val messageConsumer = new KafkaMessageConsumer
}

Now we have the ApplicationController itself which contains the controller behaviour and inherits from a MessageConsumerComponent which is where it has access to a message consumer. The controller does not care how this is created and just uses it.


trait ApplicationController extends Controller with MessageConsumerComponent {

  def get(topic: String, key: String, groupId: String) = Action {
    this.messageConsumer.get(topic, key, groupId)
    Ok("done")
  }

  def send(topic: String, key: String) = Action(parse.text) {
    request =>

      val body: String = request.body

      this.messageConsumer.send(topic, key, body)

      Ok("received: " + topic + ", " + key + "," + body)
  }
}

The message consumer is implemented as a pair of traits, the first as the interface which can be mocked and the second which is used in the cake pattern.

trait MessageConsumer {
  def get(topic: String, key: String, groupId: String)
  def send(topic: String, key: String, message: String)
}

trait MessageConsumerComponent {

  val messageConsumer: MessageConsumer

  class KafkaMessageConsumer extends MessageConsumer  {
    def get(topic: String, key: String, groupId: String) { ... }
    def send(topic: String, key: String, message: String) { ... }
  }
}

This MessageConsumerComponent defines a messageConsumer field which will be accessible by any classes which inherit from the trait and can be set to either the KafkaMessageConsumer or a mock object. That's all there is to this implementation, so let's take a look at how this is done in a unit test.

trait TestEnvironment extends MessageConsumerComponent with ApplicationController with Mockito
{
  val messageConsumer = mock[KafkaMessageConsumer]
}

@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification with TestEnvironment {

  "getting by topic" should {

    "return list of messages" in {
      val result = this.get("topic","key","group")(FakeRequest())
      there was one(messageConsumer).get("topic", "key", "group")
      status(result) must beEqualTo(OK)
    }
  }
}

I've followed Jonas' example in using a TestEnvironment object, but to avoid creating an extra component for the Application my system under test is the TestEnvironment. It might be worth changing this later but to keep things simple I've chosen this way.

There were a further two implementations I came across which are here and here. You might prefer one of those implementations and they are worth a read.

I am new to Scala so if there are errors, design issues or a Scala faux pas please let me know!

@naeemkhedarun

Excluding jmxtools with SBT

If you are using Scala and SBT you might experience this problem. In particular I was referencing Apache Kafka and found it trying to resolve the jmxtools which has been removed from the maven repository.

It took me a while to find the syntax to exclude this from my build.sbt file, but here it is:

libraryDependencies ++= Seq(
  "org.apache.kafka" % "kafka_2.10" % "0.8.0" excludeAll(ExclusionRule(organization = "com.sun.jdmk"),
                                                         ExclusionRule(organization = "com.sun.jmx"))
)

For more information you can check out the SBT documentation here.

Older Posts