Grails tip: refactoring your URLs
On the current project I am working we use a lot of integration tests. For you non-Grails users out there, Integration tests test your Controller APIs, your Services and any persistence that might happen all very neatly. The only slice of the cake they don’t test from a back end perspective are your Grails filters for which you’d need something like a functional test. In the Grails world, Controller API’s are mapped to URL requests in the URLMappings.groovy file. This is just a simple Groovy to configure which HTTP request go to which Controller.
For example:
class UrlMappings { static mappings = { "/sports/rugby/ball" (controller: "rugbyBall", action = [POST: "createBall", DELETE: "removeBall", GET: "getBall"]) ...
So in the above example, the HTTP request /sports/rugby/ball
will go to the RugbyBallController
and will go to the methods: createBall(), deleteBall(), getBall()
depending on weather the request is a GET, POST
or DELETE
. Now suppose, you have your project all set up to server up the CRUD operations for the rugby ball and after a few hectic sprints some software entropy creeps and you need to refactor your Controller APIs but before you race ahead and do that your project manager looks you in the eye and says: “You must support all existing APIs as clients are using them”.
This is how generally refactoring works in the real world when things go into production. There is always a phase of supporting the old and new, deprecating the old and then when everyone is happy removing it. Anyway, you begin by updating your URLMappings.groovy
class UrlMappings { static mappings = { // Old APIs "/sports/rugby/ball" ( controller: "rugbyBall", action = [POST: "oldCreateBall", DELETE: "oldRemoveBall", GET: "oldGetBall"]) ... // New APIs "/sports/rugby/v2/ball" ( controller: "rugbyBall", action = [POST: "createBall", DELETE: "removeBall", GET: "getBall"]) ...
The URLMappings.groovy
show the old and the new. The old APIs are going to controller methods that you have renamed. Clients using these APIs are not impacted because they only send HTTP requests, they do not know which Controller is behind these endpoints. The old APIs already have really good integration tests and our project manager has mandated that the new APIs must have similar quality integration tests before they go anywhere near pre-production.
def "test adding a single item to your cart"() { setup: "Set up the Cart and Landing Controller" //... when: //... rugbyBallController.oldGetBall(); rugbyBall = JSON.parse(rugbyBallController.response.contentAsString) then: rugbyBall.isOval();
Mr. Project manager says: “I want all these new tests added by Friday or you are not going for a pint after work. You need a quick way to get your integration tests done”. Thinking about that cool lager and its quenching effect on the back of your throat, you remember Groovy’s excellent support for invoking methods dynamically where you can specify the name of the method as a variable.
myObject."$myMethod"() // myMethod is a Groovy String variable.
In the above code snippet, myMethod
is a variable that corresponds to the name of method you want to invoke on myObject
. “$myMethod” means, evaluate the variable myMethod (which of course will be the method name), the () of course just means invokes the method. Eureka moments happens when you remember that the old and new APIs will return the exact same JSON. All you need to do is run the same test twice, once for the old code and once for the new. Since you are using the spock framework for your integration tests that’s easily achieved using a where
block.
def "test adding a single item to your cart"(String method) { setup: "Set up the Cart and Landing Controller" //... when: //... rugbyBallController."$method"(); rugbyBall = JSON.parse(rugbyBallController.response.contentAsString) then: rugbyBall.isOval(); where: method = ["oldGetBall", "getBall"]
Happy days. Now go off and drink that lager.
Reference: | Grails tip: refactoring your URLs from our JCG partner Alex Staveley at the Dublin’s Tech Blog blog. |