GrUnit: Groovy way of unit testing
Join the DZone community and get the full member experience.
Join For FreeThere is continuation of this article now
We, Java/Groovy developers, write unit tests all the time. I believe there is no need to convince anybody why is that important. My goal today is to show small but convinient experimental tool included in to Groovy++ (with potential to be backported in to Groovy Core). You can find more information about GrUnit and Groovy++ at Groovy++ project homepage
So what is GrUnit?
GrUnit is Groovy AST (abstract syntax tree) transformation, which allows just two things
- writing JUnit tests a little bit less verbose
- making tests executable as Groovy scripts
Let us start with example
@Typed package org.mbte.groovypp.compiler
import org.codehaus.groovy.tools.FileSystemCompiler
import org.codehaus.groovy.control.CompilerConfiguration
def finder = new FileNameFinder ()
String [] names = finder.getFileNames("./StdLibTest/tests/", "**/*Test.groovy")
new FileSystemCompiler (new CompilerConfiguration()).compile (names)
Looks like regular Groovy script, right? And it is regular Groovy script. But at the same time it is JUnit test case with just one test. Here is output after execution of the script.
.
Time: 3.558
OK (1 test)
Educated reader can notice that this is normal output of junit.textui.TestRunner. That's correct. Our class extends brilliant GroovyTestCase and main method of the class simply calls test runner. Of course, body of our script becomes body of the only one test in the test case.
The only thing we need to do is to place the script in a file with .grunit extension.
How do we achive that? Magic?
Of course there is no magic involved. The tecnique we use (extremely powerful and very popular in Groovy world) is compile time AST transformation. AST transformation is possibility to extend compiler behavior with plugins, which at some stages of compilation modify interanl presentation of the code.
Transformations we do in our case
- our script class should extend GroovyTestCase
- main method should call junit.textui.TestRunner
- body of the script should be transformed in to public void testXXX () method
Now let us add one more test to our script. It can be done very easy by adding testXXX {...} call at any place on top level of our script
testTestsExists {
assertTrue new File("./StdLibTest/tests/").exists()
}
def finder = new FileNameFinder ()
String [] names = finder.getFileNames("./StdLibTest/tests/", "**/*.groovy")
new FileSystemCompiler (new CompilerConfiguration()).compile (names)
And voila! Output of our script now looks differently - we have two tests running
..
Time: 3.372
OK (2 tests)
You want more? Our pleasure
testTestsExists {
assertTrue new File("./StdLibTest/tests/").exists()
}
testSuperclassName {
assertEquals "groovy.util.GroovyTestCase", this.class.superclass.name
}
def finder = new FileNameFinder ()
String [] names = finder.getFileNames("./StdLibTest/tests/", "**/*.groovy")
new FileSystemCompiler (new CompilerConfiguration()).compile (names)
Which executes already 3 tests
...
Time: 3.642
OK (3 tests)
Maybe you want to use some other base class for your test? No problem. Just add extendsTest XXX on top level of the script
extendsTest GroovyShellTestCase
testTestsExists {
assertTrue new File("./StdLibTest/tests/").exists()
}
testInstance {
assertEquals "groovy.util.GroovyShellTestCase", this.class.superclass.name
}
def finder = new FileNameFinder ()
String [] names = finder.getFileNames("./StdLibTest/tests/", "**/*.groovy")
new FileSystemCompiler (new CompilerConfiguration()).compile (names)
And of course setUp()/tearDown() are also supported. Again we just need to add it somewhere on the top level of our script. The code below is one of most important tests in Groovy++ code base - we test that we able to compile our standard library
extendsTest GroovyFileSystemCompilerTestCase
@Field FileNameFinder finder
setUp { finder = new FileNameFinder () }
tearDown { finder = null }
testFinderNotNull { assertNotNull finder }
testCompilerNotNull { assertNotNull compiler }
def names = finder.getFileNames("./StdLib/src/", "**/*.groovy")
names.addAll(finder.getFileNames("./StdLib/src/", "**/*.java"))
compiler.compile (names as String[])
But the maybe most interesting part of story is that very often you don't need setUp/tearDown. Let us rewrite test above using so called accumulated tests
extendsTest GroovyFileSystemCompilerTestCase
testCompilerNotNull { assertNotNull compiler }
def srcDir = "./StdLib/src/"
testSrcExists {
assertTrue new File(srcDir).exists()
}
def finder = new FileNameFinder()
checkNamesNonEmpty {
def names = finder.getFileNames(srcDir, "**/*.groovy")
names.addAll(finder.getFileNames(srcDir, "**/*.java"))
assertFalse names.empty
}
compiler.compile (names as String[])
Accumulated tests is a way to intermix script code with testXXX{...} tests. All script code before testXXX{} statement will be included in to the body of newly generated test method. We can also use chackXXX{...} instead of testXXX{...}. It will also lead to creation of new test method but code of closure inside checkXXX will be accumulated for use by later tests.
The main point is to have very easy way to split tests in to more granular parts for ease of debugging. Let us see what previous code snippet replace. Probably our code looks a little bit nicer
class CompileStdLibTest extends GroovyFileSystemCompilerTestCase {
void testCompilerNotNull () {
assertNotNull compiler
}
void testSrcExist () {
def srcDir = "./StdLib/src/"
assertTrue new File(srcDir).exists()
}
void testNamesNonEmpty () {
def srcDir = "./StdLib/src/"
def finder = new FileNameFinder ()
def names = finder.getFileNames(srcDir, "**/*.groovy")
names.addAll(finder.getFileNames(srcDir, "**/*.java"))
assertFalse names.empty
}
void test$main {
def srcDir = "./StdLib/src/"
def finder = new FileNameFinder ()
def names = finder.getFileNames(srcDir, "**/*.groovy")
names.addAll(finder.getFileNames(srcDir, "**/*.java"))
assertFalse names.empty
compiler.compile(names as String[])
}
static void main (String [] args) {
junit.textui.TestRunner.run(CompileStdLibTest)
}
}
At the end I want to notice that there is nothing Groovy++ specific in GrUnit (except it is written in Groovy++ and included in to Groovy++ distro), so it can be easily ported in to Groovy Core if at some point we will decide it make sense.
I hope it was not boring. Thank you for reading and till next time.Opinions expressed by DZone contributors are their own.
Comments