logo

MockFor(March): Unit Testing Grails Controllers

logo

Alrighty team, it’s time for a grab-bag of Grails Controller unit testing fragments to get you started. Most of these have been shamelessly stolen from the Graeme’s Grails.org wiki sample code, and spiced up to suit.

On the testing board today is our LoginController, since it’s pretty low hanging fruit for simple test cases that showcase the common stuff you’re likely to want to do.

Let’s start with the index method, that’s gotta be a snack right?

    def index = {redirect(action: 'form')}

Not much to do. Need to handle the redirect() call, so let’s add some setup/teardown code to tackle that one:

    def redirectParams

    /** Setup metaclass fixtures for mocking. */
    void setUp() {
        redirectParams = [ : ]
        LoginController.metaClass.redirect = { Map args -> redirectParams = args  }
    } 

    /** Remove metaclass fixtures for mocking. */
    void tearDown() {
        def remove = GroovySystem.metaClassRegistry.&removeMetaClass
        remove LoginController
    }

Now that we’ve mocked out the redirect stuff, testing becomes pretty straightforward.

    void testIndexRedirect() {
        LoginController lc = new LoginController()
        lc.index()
        assertEquals "form" , redirectParams.action
    }

Piece of cake. But what about something a little trickier? Something that involves request params, sessions, flash, loggers, all that other stuff? Well let’s enhance our setup routine to cater for the common stuff:

def session
    def params
    def redirectParams
    def flash

    /** Setup metaclass fixtures for mocking. */
    void setUp() {

        session = [ : ]
        LoginController.metaClass.getSession = { -> session }

        params = [ : ]
        LoginController.metaClass.getParams = { -> params }

        redirectParams = [ : ]
        LoginController.metaClass.redirect = { Map args -> redirectParams = args  }

        flash = [ : ]
        LoginController.metaClass.getFlash = { -> flash }

        def logger = new Expando( debug: { println it }, info: { println it },
                                  warn: { println it }, error: { println it } )
        LoginController.metaClass.getLog = { -> logger }

    }

Alright, now we’re cookin! We’ve mocked out the logger with some funky Expando-ism, and we’ve created readable/writable maps for the request/session/flash/redirect stuff. This will probably end up in a ControllerTests base class, right… but let’s keep on trucking…

With the trickier stuff in place, let’s try our hand at a logout.. Here’s the method under test:

    def logout = {
        if (session.account) {
            session.account = null
        }
        redirect(uri: "/${params.blog}/")  // to the blog they logged out from
    }

So we’re going to want to prepopulate that session.account business, but since I’m just using a map for the session mock, that’s going to be a cinch:

	void testLogout() {

        session['account'] =  "user"
        params['blog'] = 'demo'

        LoginController lc = new LoginController()
        assertNotNull lc.session.account

        lc.logout()

        assertEquals "/demo/", redirectParams.uri
        assertNull lc.session.account
    }

Giddy up. We’re a long way toward a fully exercised LoginController, but we’ve missing the Jewel in the Crown – our login() routine.

	def login = {LoginCommand cmd ->

        log.debug "Attempting login"
        if (cmd.hasErrors()) {
            redirect(action: 'form')
        } else {

            Account account = Account.findByUserIdAndPassword(cmd.userId, cmd.password.encodeAsSha1())
            if (account) {
                session.account = account
             } else {
                session.account = null
                flash.loginError = "Invalid username or password. Please try again"
            }

            redirect(uri: "/${params.blog}/")

        }

    }

That’s going to require some domain class mocking on Account, but we’re all getting a little more comfortable with ExpandoMetaClass as we go, so it’s no rocket science:

   void testLogin() {

        String.metaClass.encodeAsSha1 = { -> delegate }

        params['blog'] = 'demo'

        Account.metaClass.static.findByUserIdAndPassword = { String userId, String password ->
           if (userId == 'glen' && password == 'sesame' ) {
                return [ userId: 'glen' ]
           } else {
                return null
           }

        }

        LoginCommand.metaClass.hasErrors = { false }

        LoginCommand goodUser = new LoginCommand(userId: 'glen', password: 'sesame')

        LoginController lc = new LoginController()
        lc.login(goodUser)

        assertEquals "glen", session.account.userId
        assertEquals "/demo/", redirectParams.uri
        assertNull flash.loginError

        LoginCommand badUser = new LoginCommand(userId: 'glen', password: 'unlucky')
        lc.login(badUser)

        assertNull session.account
        assertNotNull flash.loginError

    }

Ok… Tiny bit of rocket science at the start where I mess with metaClass on String to add my SHA1 codec stuff. But other than that, just creating standard Command and Controller objects and setting/check values in my mocked session/request/etc. Really that testLogin() should be split into testGoodLogin() and testBadLogin(), but I’m rolling into one here just for ease of explanation. You can view the final class on google code.

Well that’s a good lot of Controller testing tricks ofr just one day. If you’ve got your own ideas, feel free to add them here. Next up we’ll turn our hand to services, but I reckon they’ll be more straightforward than this…

Happy Testing!

12 Responses to “MockFor(March): Unit Testing Grails Controllers”

  1. Kevin says:

    Thanks for the great tutorial. While this clears up the way things currently are with testing controllers, it just seems like it shouldn’t be so verbose. It’s not difficult, just really painful. Thanks again, I’m really liking the series.

  2. Graeme Rocher says:

    I think we should have a new ControllerTestCase that has a method like

    def withMockController(Class c, Closure c)

    that does all of the setting up of params/flash/etc.

    Then you could just do

    withMockController(LoginController) {
    def controller = new LoginController()
    lc.login(goodUser)

    assertEquals “glen”, session.account.userId
    assertEquals “/demo/”, redirectParams.uri
    assertNull flash.loginError

    LoginCommand badUser = new LoginCommand(userId: ‘glen’, password: ‘unlucky’)
    lc.login(badUser)

    assertNull session.account
    assertNotNull flash.loginError
    }

    Thoughts? JIRA issue? ;-)

  3. Mike says:

    Great stuff Glen! +1 for a Grails ControllerTestCase (http://jira.codehaus.org/browse/GRAILS-2629)

  4. Kevin says:

    Hey Graeme, looks like a definite improvement. Pulling the boilerplate out is always nice. Sorry to complain without offering a solution, but I didn’t know you were everywhere at once ;) . Here’s the JIRA: http://jira.codehaus.org/browse/GRAILS-2630.

  5. Felix says:

    from a grails lover, but an expando newbie:

    In the following situation:
    a) LoginController is a subclass of another controller ApplicationDateController
    b) in LoginController, there is a beforeInterceptor = [action:super.&loadDate] (sets the date)
    c) in LoginController.logout we call ‘super.date’ at some point

    Question: How can we mock ‘super.date’ in our UnitTest ?

    I tried the following:
    LoginController.metaClass.super.date = someDate
    ApplicationDateController.metaClass.date = someDate

    but got NPE when running my unit test

  6. Brad says:

    Really excellent and tremendously helpful Glen, thanks! Unit (non db integration) testing has really been for me one of the only difficulties (or “pains”) I’ve encountered with Grails. Your approach is not only providing examples to the rest of us, but (as you mentioned in your kickoff entry) also illustrating the ExpandoMetaClass grooviness.

    Much appreciated…

  7. Bobby says:

    I’m a grails/groovy newcomer. The examples here have been wonderful at getting our unit testing off to a good start, but I’ve encountered a problem testing an abstract controller class.

    I have a test class that extends the abstract class. I’m trying to mock out a redirect call in a method in the abstract using the metaclass (TestController.metaClass.redirect).

    The first test in my testcase works just fine, but the second test (also testing redirect) fails. On exploration I found the second test seems to be using the metaclass.redirect from the first test (checking the this in the redirect closure)… so it appears that the remove is failing and that this has something to do with the abstract class.

    If I change the test class to implement the abstract method instead, everything works fine. I’m not really sure how to solve this atm.

  8. Pablo Riesgo Ferreiro says:

    Hi, thanks for the teaching, it was really usefull for my testing.
    I was wondering if anybody would know how to make integration tests for controllers with services. I followed what’s said in the official site http://grails.org/Testing+Controllers but apparently doesn’t work, at least for me…

    Thanx in advance,
    Pablo

  9. Pablo Riesgo Ferreiro says:

    Hi again, I just found that the integration tests for controllers with services works correctly, sorry for the “spam”.
    One of my services doesn’t work within the tests don’t know why yet, I think it has to do with other services linked into this first service.

    Pablo.

  10. Stephane says:

    The meta class expando stuff evaded me, I need to read to copy I have of Glenn’s book then :-)

    Google took me here..

    The problem I have is to run a findBy.. gorm method from a unit test.

    The same method runs fine from a controller.

    But it is not found from within the unit test.

  11. Claudio Ikeda says:

    Thanks!!!

  12. Deep Shah says:

    Nice post. Have posted a series on unit testing grails controller using the ControllerUnitTestCase class.

    http://gitshah.blogspot.com/2010/04/unit-testing-grails-controller-part-1.html

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

logo
logo
Powered by WordPress | Designed by Elegant Themes