Thursday 24 July 2008

Experimental testing plugin

Saw this post on the grails mailing from a grails committer, Peter Ledbrook, might be worth checking out:

Hi everyone,

I have been working on some improvements to the Grails testing
framework recently which will go into Grails 1.1. Some of the support
classes though will work with Grails 1.0.x, so I have packaged them up
as a plugin. You can install it using:

grails install-plugin testing

Before you rush to download it, please be warned that it is nowhere
near a complete implementation yet, particularly
ControllerUnitTestCase. However, I would like to solicit feedback
early on so that we can really nail the problems that people are
having in testing. And if you can supply patches, great!

So, what lurks in the plugin? I'm planning to get some documentation
up soon, but for now I'll cover some common use cases. Before that,
the current set of classes are designed to be used in unit tests. As a
general trend, we want to encourage people to write unit tests in
preference to integration tests. We also want to make sure that unit
tests can be run from within an IDE, i.e. there should be no
requirement on a running Grails instance.

For the first example I'll show you how to test domain constraints.
Domain classes often lack logic, and so they don't get tested.
However, plenty of errors can creep in to the constraints so it's
worth validating them.

class MyDomain {
String name
Integer age

static constraints = {
name(nullable: false, blank: false)
age(nullable: false, min: 10, max: 100)
}
}

class MyDomainUnitTests extends GrailsUnitTestCase {
void testConstraints() {
// Mock the validate() method.
registerMetaClass(MyDomain)
MockUtils.
prepareForConstraintsTests(MyDomain)

// Test that a fresh new domain instance fails validation on
// the "nullable: false" constraints.
def testInstance = new MyDomain()
def errors = testInstance.validate()
assertEquals 2, errors.size()
assertEquals "nullable", errors["name"]
assertEquals "nullable", errors["age"]

// Test the other constraints
testInstance = new MyDomain(name: " ", age: 5)
errors = testInstance.validate()
assertEquals 2, errors.size()
assertEquals "blank", errors["name"]
assertEquals "min", errors["age"]
}
}

The main things to note here are:

1. We sub-class GrailsUnitTestCase
2. "registerMetaClass()" and "MockUtils.prepare...()" add the
validate() method to the domain class
3. We create instances of the domain class, call validate(), and check
whether any errors were found

On (2), the two lines will be replaced by a method on
GrailsUnitTestCase in the near future. On (3), the validate() method
returns a map of validation errors. Note that we check for the name of
the constraint, not the i18n error code associated with the
constraint.

That's it for domain constraints. For other unit tests, such as for
services and controllers, GrailsUnitTestCase provides the method
"mockDomain(Class, List)":

void testMethod() {
mockDomain(MyDomain, [
new MyDomain(name: "John Smith", age: 35),
new MyDomain(name: "Alice Smith", age: 64),
new MyDomain(name: "Irene Pane", age: 22),
new MyDomain(name: "Patrick Rose", age: 45) ])

def testService = new MyService()
testService.doSomething()
...
}

The method injects working versions of the dynamic methods and
properties that normally go with domain classes, in particular the
dynamic finders. Where appropriate, these injected methods/properties
use the given list of domain instances as a source of data. For
example, if MyService.doSomething called a dynamic finder like this:

MyDomain.findByNameLike("%Smith")

the mock property would return a list containing the "John Smith" and
"Alice Smith" MyDomain instances in that order. Another useful method
is "mockFor()" which returns an object that you can use pretty much
like the Groovy MockFor class:

def mockControl = mockFor(MyDomain)
mockControl.demand.save(1..1) {-> return true}
mockControl.demand.static.findByName(1..1) { name -> return [] }

The best thing about this method is that it works seemlessly with
"mockDomain()", i.e. you can readily override the methods provided by
"mockDomain()" via the mock object returned by "mockFor()".

Finally, there is a ControllerUnitTestCase, but it is in the very
early stages of development. I recommend you only use it if you're
willing to patch it up with the functionality you need. It will
automatically inject all the normal controller properties and methods,
but not much else. However, one nice feature I have implemented
already is the ability to set the body of the request to some XML
(either a string or builder markup), particularly useful for REST
controllers based on XML. I need to add support for JSON too.

That's it for now. I would certainly give GrailsUnitTestCase a go
because I have already found it much easier to write Grails unit tests
than I used to. If you want to raise issues or provide patches, please
add them to the main Grails JIRA, setting the fix version to 1.1 and
assigning them to me (username "pledbrook").

Cheers,
Glenn

No comments: