Testing Acegi Security with mock objects in Grails

I’ve always had the “will” to test my code in Grails, but have had little time to dedicate to writing tests, so at the first sign of difficulty, I’ve abandoned it.  This is bad.  I know it’s bad, but sometimes it can’t be helped.  Part of my difficulties have been due to me trying to get my tests to do something which is not unit testing.  It’s really easy to think you are writing unit tests when in reality, you are writing poor integration tests.  One area in particular that I’ve had trouble is with a small app that tracks time, and produces invoices, and recently I’ve had the luxury of being able to revisit the code and make amends for my previous omissions.   I want to share the revelations I’ve had with you here in the hope that it proves useful to someone.

Mocking with Acegi Security

In my application, I lean quite heavily on Acegi to work out the correct thing to do in a certain situation. I also always endeavour to push application logic away from controllers and into services, which I believe makes things much more reusable. For example, I have a service which supports the Contracts domain class. This service has a method which returns an “appropriate” list of contracts for the currently logged on user. I use the authenticationService provided by the Acegi plugin to work out who the current user is and what roles they have been granted, like this:

def getContractList = {
def contracts
def cuser = authenticateService.principal()?.getDomainClass()

if (authenticateService.ifAnyGranted("ROLE_ADMIN")){
contracts = Contract.list()
} else if (authenticateService.ifAnyGranted("ROLE_APPROVER")){
def c = Contract.createCriteria()
contracts = c {
project {
approvers {
idEq(cuser?.id)
}
}
}
} else if (authenticateService.ifAnyGranted("ROLE_USER")){
def c = Contract.createCriteria()
contracts = c {
users {
idEq(cuser?.id)
}
}
}
}

Testing the above service call evaded me for a while; the problem was that I was over thinking it. Each time I would attempt to write some tests, some problem would crop up and I’d rapidly run away from my obligations to TDD. I looked at various approaches, like mocking the authenticateService object and mocking the principal it returned. I decided that was far too much like hard work. I looked at approaches such as the one here and again it didn’t really work out for me. The best resource I’ve found for writing Grails tests is in the Groovy documentation. Here’s what I did instead:

void testContractListforUser() {
def contractService = new ContractService()
//use an expando to mock authenticateService
def mockAuthenticateService = new Expando()
//use a combination of closure and literal map coercion to mock getDomainClass
mockAuthenticateService.metaClass.principal = {[getDomainClass:{new User(id:1)}]}
mockAuthenticateService.metaClass.ifAnyGranted = {param -> param == 'ROLE_USER'}
contractService.authenticateService = mockAuthenticateService
def contractList = contractService.getContractList();
//put assertions here
}

Of course, it would be lovely if that worked but, alas it doesn’t. The problem is of course mocking of criteria or HQL queries is not supported for unit tests. I would have at this point thrown my toys out the pram and packed in testing again, but I had some time on my hands. The real problem here is not that mocking criteria is not supported, but that my code as I’ve written it is not testable. What should I do? Give up? Me? Never. When you are writing testable software, you have to go about it in the right way, and the right way in this case is to abstract out the code which interfaces with the external system. Actually, writing these tests has made me think that I’m pushing too much repressibility into services, rather than putting this functionality where it more naturally belongs; in the domain class. In doing this, I’ve cleaned up the service and made the code much more testable.

The service method currently looks like this:

def getContractList = {
def contracts = []
def cuser = authenticateService.principal()?.getDomainClass()

if (authenticateService.ifAnyGranted("ROLE_ADMIN")){
contracts = Contract.listAllActiveContracts()
} else if (authenticateService.ifAnyGranted("ROLE_APPROVER")){
contracts = Contract.listContractsForApprover(cuser.id)
} else if (authenticateService.ifAnyGranted("ROLE_USER")){
contracts = Contract.listContractsForUser(cuser.id)
}
return contracts
}

The code to test this looks like this:

void testContractListforUser() {
def contractService = new ContractService()
//use an expando to mock authenticateService
def mockAuthenticateService = new Expando()
//use a combination of closure and literal map coercion to mock getDomainClass
mockAuthenticateService.metaClass.principal = {[getDomainClass:{new User(id:1)}]}
mockAuthenticateService.metaClass.ifAnyGranted = {param -> param == 'ROLE_USER'}
contractService.authenticateService = mockAuthenticateService
def c = mockFor(Contract)
c.demand.static.listContractsForUser(){userId -> assert(userId == 1)}
def contractList = contractService.getContractList();
c.verify()
}

What I’m doing here much more closely aligns with the intention of unit testing, which is to make sure that your code is behaving the way you expect it to (as defined by the tests). The intention of unit testing is not so much to make sure that code “works”, just to make sure the bits you control are doing what you are expecting. If what you expect turns out to be incorrect, then you will refine your tests. How do you find out whether you got it right? Well that’s the job of integration and acceptance testing and the subject of a future blog post no doubt.

This post would not be complete without mentioning the very fine GMock library which does allow you to partially mock objects enabling you to be able to mock for createCriteria etc. This approach is documented here. I don’t think that this capability would add much in my case. I think that the approach of testing whether the queries work can safely be delayed to the integration testing phase without loosing much.

Leave a Reply

Your email address will not be published. Required fields are marked *