Only a Masochist Would Write Unit Tests in Java. Be Smarter, Use Groovy (or Scala...).
I like writing unit tests but Java doesn't make it particularly easy. Especially if you need to create objects and object trees, transform objects for checking them etc. I miss a lot a conscise, powerful syntax, literals for regular expressions and collections, conscise, clojure-based methods for filtering and transforming collections, asserts providing more visibility into why they failed. But hey, who said I have to write tests in the same language as the production code?! I can use Groovy - with its syntax being ~ 100% Java + like thousand % more, optional usage of static/dynamic typing, closures, hundreds of utility methods added to the standard JDK classes and so on. Groovy support for example in IntelliJ IDEA (autocompletion, refactoring ...) is very good so by using it you loose nothing and gain incredibly much. So I've decided that from now on I'll only use Groovy for unit tests. And so far my experience with it was overwhelmingly positive (though few things are little more complicated by the positives more than compensate for them). Read on to find out why you should try it too.
(The arguments here focus on Groovy but I guess similar things could be said about JRuby, Scala etc. - with the exception of Java code compatibility, which you only get in Groovy.)
Few examples
Some of the example below use some Groovy magic but don't be scared. You can write Groovy just as if it was Java and only learn and introduce its magic step by step as you need it.Bean construction:
def testBean = new Customer(fname: "Bob", sname: "Newt", age: 42)
// Java: c = new Customer(); c.setFname("Bob"); c.setSname("Newt"); c.setAge(42);
(Of course this starts to pay of if either you don't want to create a constructor or if there are "many" properties and you need to set different subsets of them (constructor with 4+ arguments is hard to read).)
Reading a file:
assert test.method() == new File("expected.txt").getText()
// Java: buffered reader line by line ...; Note: == actually uses equals()
Checking the content of a collection/map:
assert customerFinder.findAll().collect {it.sname}.sort() == ["Lizard","Newt"]
// Java: too long to show here (extract only surnames, sort them, compare ...)
assert getCapitalsMap() == ["UK" : "London", "CR" : "Prague"]
Regular expressions:
assert ("dog1-and-dog2" =~ /dog\d/).getAt([0,1]) == ["dog1", "dog2"]
- Or more fail-safe regexp:
assert ("dog1-and-dog2" =~ /dog\d/).iterator().toSet() == ["dog1", "dog2"].toSet()
- With a match group:
assert ("dog11-and-dog22" =~ /dog(\d+)/).iterator().collect({it[1]}).toSet() == ["11", "22"].toSet()
What is Groovy?
Groovy 1.8 is a mature scripting language for the JVM written as an extension of Java with optional dynamic typing, closures, and more. It's now developed by SpringSource, the company behind the Spring framework.As mentioned, nearly all Java code is a valid Groovy code with basically one exception: To initialize an array you can't use { ... } (for that would be a closure), you must use [...] instead (notice that by default it actually creates a List, only when assigned to an array variable or casted to array will it produce an array). Make sure to check the few common gotchas before you dive into using Groovy.
You can experiment with Groovy online in the GAE Groovy console.
Groovy features especially beneficial for testing
General- Dynamic typing at request => conscise, avoids code cluttered with casts
Files: Read all the text with one call, withReader { .. } etc.
Testing/advanced:
- assert- you can use the Java keyword assert instead of JUnit methods, upon failure Groovy will provide you with pretty good info of what went wrong:
Assertion failed:
assert config.getResolvers()) == ["h:dataTable" : resolver, "my:dt2" : null] | | | | | | false | | | MyResolver@731d2572 | [h:dataTable:MyResolver@731d2572, my:dt2:MyResolver@731d2572] LocalVariableConfiguration@7e859a68 - Here-docs: embed multi-line strings easily in a test (also supports replacing references like $variable with values)
- Implementing interfaces with a map (map coercion)
- Use expandos to define dynamic beans (similarly to JavaScript, you instantiate an Expando and just add properties and closures as methods) - as described on the linked page, expandos and maps are usually enough to replace a mocking library
- Build-in mocking
- With Groovy you can of course also use Spock, the excellent specification & testing framework
Complications
- Neither JUnitMax nor Infinitest (in IntelliJ) seem to support Groovy test cases
- You need a decent IDE such as IntelliJ IDEA
- If using Maven, you have to explicitely configure the GMaven plugin (esp. with a newer Groovy version)
- IntelliJ 10.5: Click & alt+enter on a non-existing method to create it only works if the target type is a nested class within the test, not if it is a standalone Java class (so I just create my class there and when done TDDing I extract it into a top-level Java class)
Conclusion
Groovy makes test writing much more productive and thus developers happy. I intend to use on all my open source projects and to try push it into our commercial projects too. You should give it a try too!Addendum
I don't meen to say that you should start writing all your tests in Groovy (or Scala, JRuby, ..) right now and that whoever writes them in Java is a #!&$ (insert a pejorative adjective of your choosing). If you are perfectly happy with Java, that's fine. If you don't like Groovy (Scala/JRuby/...) or have a negative experience with it, that's fine too. But if you feel after reading this that using Groovy/... might make you a more productive and happy developer then I humbly recommend that you try it and evaluate for yourself whether it is a positive change or not.And yes, Kent Beck and Chuck Norris do write their tests in Java.
I apologize to all who feel irritated or offended by this post. That was not intended. Please eradicate it from your memory.
Warning
Unfortunately I have to say that despite all the benefits of Groovy I wouldn't recommand using it in Eclipse (3.7). The support there is very week, for example basic refactorings like creating a method/class used in the test but not really existing isn't supported. Contrary to that, IntelliJ is perfectly suitable for using Groovy. I haven't tried NetBeans.Additional Links
- DefaultGroovyMethods.java (src): defines the methods added to all objects
- DefaultTypeTransformation.java: methods used internally to convert between types and when using "as aType" - useful to know what as can convert