Software Dev&QE, Author, Pilot... Software engineer interested in bleeding-edge technologies, always craving new toys to play with especially in the Java world. Working as a supervisor in the JBoss division of Red Hat, propagating JBoss projects and products in the Czech Republic by teaching courses at local universities and writing blog posts. Occasional freelance consultant and trainer. Likes to save moments of everyday life using a camera, has a great passion for flying high in the sky and more. Martin has posted 2 posts at DZone. You can read more from them at their website. View Full User Profile

Testing Groovy Classes with ScalaTest

12.08.2012
| 3413 views |
  • submit to reddit

The other day I read about ScalaTest. I liked the way the tests look like. And I started thinking where to use it. Today a colleague came to me asking me if he can add some tests to our small tool developed in Groovy. This ringed the bell and it turned to a calling. So here is the ultimate solution for integrating Groovy and ScalaTest.

I started with a simple Groovy class org.jboss.qa.SuperUtil (yes, I'm a JBoss Quality Assurance guy ;-)) with a single static method to add two numbers. The class is located in the src directory.

def class SuperUtil {

    def static add(int a, int b) {
        return a + b
    }

}

Second, I wrote a org.jboss.qa.test.SuperUtilSuite according to the ScalaTest quickstart.

import org.scalatest._
import org.jboss.qa.SuperUtil

class SuperUtilSuite extends FunSuite {

  test("SuperUtil add should return addition of two numbers") {
    assert(SuperUtil.add(2, 3) == 5)
  }
}

As you can see, I directly imported the Groovy class.

We had two basic requirement:

  1. We wanted to be able to run the tests from a Groovy script.
  2. We wanted to avoid any unnecessary libraries hanging around.

So I started with a Groovy script test.groovy with low hanging Grapes:

@Grapes([
  @Grab(group='org.scala-lang', module='scala-library', version='2.9.2'),
  @Grab(group='org.scalatest', module='scalatest_2.9.1', version='1.8'),
])

I realized that ScalaTest wants only compiled test classes (is there any way around?). To be able to compile the SuperUtilSuite, I must compile SuperUtil first. So things started to get complicated. In Groovy there is a very easy way of using various tools like compilers - AntBuilder. Let's start with defining all the Ant tasks we will need.

def ant = new AntBuilder()
ant.taskdef(name: 'groovyc', classname: 'org.codehaus.groovy.ant.Groovyc')
ant.taskdef(resource: 'scala/tools/ant/antlib.xml')
ant.taskdef(name: 'scalatest', classname: 'org.scalatest.tools.ScalaTestAntTask')

None of these tasks can be defined because none of these resources are on the classpath. I must add Scala compiler to Grapes and make Grapes load the classes with the default system classloader. So the updated Grapes configuration reads like this:

@Grapes([
  @Grab(group='org.scala-lang', module='scala-library', version='2.9.2'),
  @Grab(group='org.scalatest', module='scalatest_2.9.1', version='1.8'),
  @Grab(group='org.scala-lang', module='scala-compiler', version='2.9.2'),
  @GrabConfig(systemClassLoader = true)
])

Now I can create target directories and compile Groovy classes.

ant.mkdir(dir: 'target/test-classes')
ant.mkdir(dir: 'target/classes')

ant.groovyc(srcdir: 'src', destdir: 'target/classes')

Do not forget to specify correct package in your sources. I forgot that and it took me a while to realize why the compiler stores the class directly under target/classes. What a stupid mistake.

Compilation of the Scala test class is not that straight forward. This needs the base Scala library, the ScalaTest library, the Groovy classes to be tested, and the Groovy library (a Groovy class usually depends on GroovyObject for example). The question is how to add the dependencies imported with Grapes and how to add the Groovy library.

Classes imported with Grapes can be obtained with Grape.resolve(). It returns an array of all the libraries imported with @Grapes annotation(s).

There are two ways for obtaining the Groovy library. One can use Grapes as well, but over time, it might be getting another version of Groovy than it is used to run the script and this can lead to ugly exceptions. So I decided to directly lookup groovy-all-*.jar in the $GROOVY_HOME/embeddable directory. I did not need invoke dynamic so I removed the jar file with 'indy' in its name from the classpath.

ant.scalac(srcdir: 'test', destdir: 'target/test-classes', fork: false) {
  classpath {
    pathelement(location: 'target/classes')
    fileset(dir: System.getenv('GROOVY_HOME') + '/embeddable') {
      include(name: 'groovy-all-*.jar')
      exclude(name: '*indy*')
    }
    Grape.resolve(new HashMap()).each {
      pathelement(location: new File(it).absolutePath)
    }
  }
}

Finally, I was able to use the ScalaTest Ant task to run the test. It is almost the same as calling the Scala compiler but it has one more classpath element - the complied test classes.

ant.scalatest(suite: 'org.jboss.qa.test.SuperUtilSuite') {
  runpath {
    pathelement(location: 'target/classes')
    pathelement(location: 'target/test-classes')
    fileset(dir: System.getenv('GROOVY_HOME') + '/embeddable') {
      include(name: 'groovy-all-*.jar')
      exclude(name: '*indy*')
    }
    Grape.resolve(new HashMap()).each {
      pathelement(location: new File(it).absolutePath)
    }
  }
}

In the end, everything went as expected.

[scalatest] Run starting. Expected test count is: 1
[scalatest] SuperUtilSuite:
[scalatest] - SuperUtil add should return addition of two numbers
[scalatest] Run completed in 115 milliseconds.
[scalatest] Total number of tests run: 1
[scalatest] Suites: completed 1, aborted 0
[scalatest] Tests: succeeded 1, failed 0, ignored 0, pending 0
[scalatest] All tests passed.

Now I could just use some report writers and that's all folks.

You can get complete source code from GitHub.

Published at DZone with permission of its author, Martin Vecera.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)