Last updated by
4 years ago
Page: Testing Plugin, Version:8
Testing Plugin
Unit testing in the 1.0.x line of Grails is not the easiest thing in the world. If you want to test domain classes, controllers, or even services there are so many things you need to mock and set up. Without a rigorous approach to it test can quickly degenerate into a mess. On top of that, custom meta-class programming sometimes leaks into the integration tests, causing odd failures. This difficulty often results in users going straight for integration tests, despite unit tests having the benefit of speed and the ability to run from within an IDE.The Testing Plugin targets these weaknesses by providing a set of classes that make testing many of Grails' artifacts really easy, while providing plenty of flexibility.The classes in the testing plugin are being incorporated into Grails 1.1, so if you have a recent 1.1 SNAPSHOT or revision from the Subversion branch, then you don't need to install the plugin.
Getting started
Installing the plugin is trivial:grails install-plugin testing
The framework
The core of the testing plugin is thegrails.test.GrailsUnitTestCase class. This is a sub-class of GroovyTestCase geared towards Grails applications and their artifacts. It provides several methods for mocking particular types as well as support for general mocking a la Groovy's MockFor and StubFor classes. Let's start by looking at how you can use GrailsUnitTestCase to test a simple service:
class MyService {
def otherService String createSomething() {
def stringId = otherService.newIdentifier() def item = new Item(code: stringId, name: "Bangle")
item.save()
return stringId
} int countItems(String name) {
def items = Item.findAllByName(name)
return items.size()
}
}GrailsUnitTestCase ?
import grails.test.GrailsUnitTestCaseclass MyServiceTests extends GrailsUnitTestCase { void testCreateSomething() { // Mock the domain class. def testInstances = [] mockDomain(Item, testInstances) // Mock the "other" service. String testId = "NH-12347686" def otherControl = mockFor(OtherService) otherControl.demand.newIdentifier(1..1) {-> return testId } // Initialise the service and test the target method. def testService = new MyService() testService.otherService = otherControl.createMock() def retval = testService.createSomething() // Check that the method returns the identifier returned by the // mock "other" service and also that a new Item instance has // been saved. assertEquals testId, retval assertEquals 1, testInstances assertTrue testInstances[0] instanceof Item } void testCountItems() { // Mock the domain class, this time providing a list of test // Item instances that can be searched. def testInstances = [ new Item(code: "NH-4273997", name: "Laptop"), new Item(code: "EC-4395734", name: "Lamp"), new Item(code: "TF-4927324", name: "Laptop") ] mockDomain(Item, testInstances) // Initialise the service and test the target method. def testService = new MyService() assertEquals 2, testService.countItems("Laptop") assertEquals 1, testService.countItems("Lamp") assertEquals 0, testService.countItems("Chair") } }
GrailsUnitTestCase . It adds all the common domain methods (both instance and static) to the given class so that any code using it sees it as a full-blown domain class. So for example, once the Item class has been mocked, we can safely call the "save()" method on instances of it. Speaking of which, what happens when we call that method on a mocked domain class? Simple: the new instance is added to the "testInstances" list we passed into the "mockDomain()" method.The next bit we want to look at is centered on the "mockFor" method. This is analagous to the MockFor and StubFor classes that come with Groovy and it can be used to mock any class you want. In fact, the "demand" syntax is identical to that used by Mock/StubFor, so you should feel right at home. Of course you often need to inject a mock instance as a dependency, but that is pretty straight forward with the "createMock()" method, which you simply call on the mock control as shown. For those familiar with EasyMock, the name "otherControl" highlights the role of the object returned by "mockFor()" - it is a control object rather than the mock itself.The rest of the "testCreateSomething()" method should be pretty familiar, particularly as you now know that the mock "save()" method adds instances to "testInstances" list. However, there is an important technique missing from the test method. We can determine that the mock "newIdentifier()" method is called because its return value has a direct impact on the result of the "createSomething()" method. But what if that weren't the case? How would we know whether it had been called or not? With Mock/StubFor the check would be performed at the end of the "use()" closure, but that's not available here. Instead, you can call "verify()" on the control object - in this case "otherControl". This will perform the check and throw an assertion error if it hasn't been called when it should have been.Lastly, "testCountItems()" in the example demonstrates another facet of the "mockDomain()" method. It is normally quite fiddly to mock the dynamic finders manually, and you often have to set up different data sets for each invocation. On top of that, if you decide a different finder should be used then you have to update the tests to check for the new method! Thankfully the "mockDomain()" method provides a lightweight implementation of the dynamic finders backed by a list of domain instances. Simply provide the test data as the second argument of the method and the mock finders will just work.GrailsUnitTestCase - the "mock*()" methods
You have already seen a couple of examples in the introduction of the "mock*()" methods provided by theGrailsUnitTestCase class. Here we will look at all the available methods in some detail, starting with the all-purpose "mockFor()". But before we do, there is a very important point to make: using these methods ensures that any changes you make to the given classes do not leak into other tests! This is a common and serious problem when you try to perform the mocking yourself via meta-class programming, but that headache just disappears as long as you use at least one of "mock*()" methods on each class you want to mock.mockFor(class, loose = false)
_General purpose mocking that allows you to set up either strict or loose demands on a class._This method is surprisingly intuitive to use. By default it will create a strict mock control object (one for which the order in which methods are called is important) that you can use to specify demands:def strictControl = mockFor(MyService)
strictControl.demand.someMethod(0..2) { String arg1, int arg2 -> … }
strictControl.demand.static.aStaticMethod {-> … }mockControl.createMock() . In fact, you can call this as many times as you like to create as many mock instances as you need. And once you have executed the test method, you can call mockControl.verify() to check whether the expected methods were actually called or not.Lastly, the call:
def looseControl = mockFor(MyService, true)mockDomain(class, testInstances = [])
_Takes a class and makes mock implementations of all the domain class methods (both instance- and static-level) accessible on it._Mocking domain classes is one of the big wins from using the testing plugin. Manually doing it is fiddly at best, so it's great thatmockDomain() takes that burden off your shoulders.In effect, mockDomain() provides a lightweight version of domain classes in which the "database" is simply a list of domain instances held in memory. All the mocked methods ( save() , get() , findBy*() , etc.) work against that list, generally behaving as you would expect them to. In addition to that, both the mocked save() and validate() methods will perform real validation (support for the unique constraint included!) and populate an errors object on the corresponding domain instance.There isn't much else to say other than that the plugin does not support the mocking of criteria or HQL queries. If you use either of those, simply mock the corresponding methods manually (for example with mockFor() ) or use an integration test with real data.mockForConstraintsTests(class, testInstances = [])
_Highly specialised mocking for domain classes and command objects that allows you to check whether the constraints are behaving as you expect them to._mockLogging(class, enableDebug = false)
_Adds a mock "log" property to a class. Any messages passed to the mock logger are echoed to the console._mockController(class)
_Adds mock versions of the dynamic controller properties and methods to the given class. This is typically used in conjunction with theControllerUnitTestCase class._mockTagLib(class)
_Adds mock versions of the dynamic taglib properties and methods to the given class. This is typically used in conjunction with theTagLibUnitTestCase class._