Saturday, May 24, 2014

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 of 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 which Controller are 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 this all new tests added by Friday or you are not going for a pint after work. You need a quit way to get your integration tests done". Thinking about that cool larger and its quenching effect on the back 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.

No comments:

Post a Comment