Tuesday, May 4, 2010

Starting with Akka part 2, Intellij IDEA, Test Driven Development

Update to this post: I have upgraded the examples to Akka 1.0-RC1. this newer post might be more helpful, or check directly at github here.

In this post I'm going to expand on the previous post's simple project, add the use of an IDE, and do some Test Driven Development with akka (with scalatest+junit)

First, check if your Project.scala file has the dependencies for scalatest and junit:


import sbt._

class Project(info: ProjectInfo) extends DefaultWebProject(info){
  override def repositories = Set(
  "Java.Net" at "http://download.java.net/maven/2",
  "jBoss" at "http://repository.jboss.org/maven2",
  "service mix" at "http://svn.apache.org/repos/asf/servicemix/m2-repo/",
  "Apache Camel" at "https://repository.apache.org/content/repositories/releases/",
  "Akka Maven Repository" at "http://scalablesolutions.se/akka/repository",
  "Multiverse Releases" at "http://multiverse.googlecode.com/svn/maven-repository/releases/",
  "GuiceyFruit" at "http://guiceyfruit.googlecode.com/svn/repo/releases/",
  "DataBinder" at "http://databinder.net/repo",
  "Configgy" at "http://www.lag.net/repo",
  ScalaToolsSnapshots)

  override def libraryDependencies = Set(

  /* servlet implementation */
  "org.eclipse.jetty"  % "jetty-server"   % "7.0.1.v20091125" % "test",
  "org.eclipse.jetty"  % "jetty-webapp"   % "7.0.1.v20091125" % "test",
                "org.scalatest" % "scalatest" % "1.0.1-for-scala-2.8.0.Beta1-with-test-interfaces-0.3-SNAPSHOT" % "test->default",
  /* camel */
  "org.apache.camel" % "camel-ftp" % "2.2.0" % "compile",
  /* akka dependencies */
  "se.scalablesolutions.akka" % "akka-kernel_2.8.0.Beta1"  % "0.8.1" % "compile",
  "se.scalablesolutions.akka" % "akka-core_2.8.0.Beta1"    % "0.8.1" % "compile",
  "se.scalablesolutions.akka" % "akka-servlet_2.8.0.Beta1" % "0.8.1" % "compile",
  "se.scalablesolutions.akka" % "akka-rest_2.8.0.Beta1"    % "0.8.1" % "compile")
  override def jettyPort = 9012
  val junit = "junit" % "junit" % "4.5" % "test"
}


As you can see I have also added a dependency to Camel FTP2, a Camel component for FTP, which I will be using in my next post.

At the moment I am using Intellij IDEA Community Edition EAP with the Scala Plugin for 2.8.0.Beta1. The EAP edition does come with a warning from jetbrains: "Please note that the quality of EAP versions may at times be way below even usual beta standards".

The Eclipse plugin is not usable at the moment, at least it didn't work for me. I did notice that IDEA was storing a +800MB index file somewhere in my home directory, which is kind of freaky, and compilation is a bit slow. I'm going to investigate later what I will lose by using a simpler editor and doing everything through sbt scripting. But for now I'm happy to use IDEA.

(Here is a good tip to add scala highlighting to gedit.)

First download the IDEA Community Edition EAP and install:


$ wget http://download.jetbrains.com/idea/ideaIC-95.54.tar.gz
$ sudo mkdir /usr/share/idea
$ sudo cp ideaIC-95.54.tar.gz /usr/share/idea
$ cd /usr/share/idea
$ tar xvf ideaIC-95.54.tar.gz
$ cd /usr/local/bin
$ sudo ln -s /usr/share/idea/idea-IC-95.54/bin/idea.sh 

  


Then run idea so that it creates a .IdeaIC90 directory in your home directory.
add a plugins directory to this directory:


$ cd ~/.IdeaIC90/config
$ mkdir plugins
  


Then download and install the latest scala plugin (at the moment scala-intellij-bin-0.3.1156.zip):


$ wget 'http://plugins.intellij.net/plugin/?action=download&id=8152' -O scala-plugin.zip
$ cp scala-plugin.zip .IdeaIC90/config/plugins
$ cd .IdeaIC90/config/plugins/
$ unzip scala-plugin.zip


Now run Idea, you should see the Scala plugin in the Plugins section. Create a new project from scratch, create it in the directory where your project is (in my case ~/akka_start):


Before I had some issues that IDEA would take the wrong scala facets and version, if that happens to you, check here:
http://www.jetbrains.net/devnet/thread/284354

Also, if the facet is not automatically included in your project, or you didn't check the Scala tickbox in the project wizard, open the .iml file in an editor (not in IDEA, it will be replaced) and add the facets so you get something like this (add the FacetManager part):


<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
  <component name="FacetManager">
    <facet type="Scala" name="Scala">
      <configuration>
        <option name="takeFromSettings" value="true" />
        <option name="myScalaCompilerJarPaths">
          <array>
            <option value="$MODULE_DIR$/lib/scala-compiler.jar" />
          </array>
        </option>
        <option name="myScalaSdkJarPaths">
          <array>
            <option value="$MODULE_DIR$/lib/scala-library.jar" />
          </array>
        </option>
      </configuration>
    </facet>
  </component>
  <component name="NewModuleRootManager" inherit-compiler-output="true">
    <exclude-output />
    <content url="file://$MODULE_DIR$">
      <sourceFolder url="file://$MODULE_DIR$/src/main/scala" isTestSource="false" />
      <sourceFolder url="file://$MODULE_DIR$/src/test/scala" isTestSource="true" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
    <orderEntry type="library" name="scala-2.8.0.Beta1" level="application" />
    <orderEntry type="library" name="akka" level="project" />
  </component>
</module>


Now add the source and test folders in the project structure dialog:


And add the dependency to the jars in lib_managed/scala_2.8.0.Beta1/compile
If you followed the previous post, rebuild project should now complete with no errors, code completion should work and you are ready to go :)

As I said before, the IDEA scala compiler is a bit slow, so at the moment I don't compile after every change in IDEA. I keep this cool sbt feature running in a terminal:



$ sbt
$ ~test-quick


The ~ makes test-quick running as a triggered action, and compiles and runs all failed or dependent tests when any source modification is done.

I wrote a small test just to check if everything is working. (You would normally of course never put the test code and the scala code together in one file, but its simpler to read here)

The test starts a "Worker" Actor, queues a few Commands, and then checks the amount of commands that are queued in the worker, and checks if all commands are executed. I'm using the MustMatchers for the nice syntax ('must be' matcher). The red-green-refactor flow is very nice while using the ~test-quick command.



import org.scalatest.Spec
import org.scalatest.matchers.MustMatchers

import se.scalablesolutions.akka.actor.{Actor}


case class Command(name: String, data: String)

case class CountCommandsQueued()

case class Execute()

case class CountResponse(count: Int)


class Worker extends Actor {
  var commands: List[Command] = Nil

  def receive = {
    case msg: Command => {
      commands = msg :: commands
    }
    case msg: Execute => {
      for (command <- commands) {
        commands = commands.dropRight(1)
      }
    }
    case msg: CountCommandsQueued => {
      reply(new CountResponse(commands.size))
    }
  }
}

class WorkerSpec extends Spec with MustMatchers {
  describe("A Worker") {

    describe("(when it queues commands)") {
      it("should have the correct number of commands queued") {

        val command = new Command("test", "data")
        val actor = new Worker()
        actor.start
        var reply: CountResponse = (actor !! new CountCommandsQueued()).getOrElse(fail())
        reply.count must be === 0

        actor ! command
        actor ! command
        actor ! command
        reply = (actor !! new CountCommandsQueued()).getOrElse(fail())
        reply.count must be === 3
        actor.stop
      }
    }
    describe("(when it executes all queued commands)") {
      it("should have no commands queued after executing") {
        val command = new Command("test", "data")
        val actor = new Worker()
        actor.start
        actor ! command
        actor ! command
        var reply: CountResponse = (actor !! new CountCommandsQueued()).getOrElse(fail())
        reply.count must be === 2
        actor ! new Execute()
        reply = (actor !! new CountCommandsQueued()).getOrElse(fail())
        reply.count must be === 0
        actor.stop
      }
    }
  }
}

No comments:

Post a Comment