Testing drupal with grails webtest

I was implementing a Drupal site with a custom module that uses a custom mysql database.  The client reported problems with a custom module.  I tried reproducting the problem, but after trying 20 some combinations out of 1000+ test elements, I wasn't able to reproduce the questionable behavior.

Enter grails with webtest.

Wait... Drupal is PHP based, why grails?  Because it's easy and super fast to get things done.  Grails takes just a couple of minutes to install, installation is a 3 step process:  1) extract 2) update path 3) set env variable.  And once it's installed, it's nothing to create a grails app to do some testing.  Typically in Java development, there's a whole bunch of overhead that you need to do for web development that includes configuration of a classpath, ant file, web.xml and a myriad of other tasks dependent upon your application.  With grails, this comes ready to go, ready to run.

 I wanted to perform a data-driven webtest.  Grails has an excellent plugin for testing web applications, and it's super easy to hit a sql database with Groovy, 1-2 lines of code easy.  I couldn't think of a product in the PHP world that would offer a way to use the results of a sql query to perform a set series of HTTP POST operations against a web application.

So to summarize, in my groovy application, a query the mysql table (this can be any database if you have the JDBC driver), and for each record, I create a new drupal account (via http post to the Drupal account create page) and go to a specify page in the Drupal CMS and verify that there is specific text on that page for that specific user.  Each page is customized based upon values in this mysql database.

Install/configure grails

I begin by creating a new grails applcation:

grails create-app lawtest

I install the webtest plugin

grails install-plugin webtest

The webtest plugin comes with htmlunit version 2.4.  HtmlUnit is a java-based library that webtest uses.  Older versions of HtmlUnit do not work with recent versions of Jquery, so we have to do a "manual upgrade".  If webtest tries to parse a page with Jquery on it, it will die with a "TypeError: Cannot find function createComment in object [object]"  More info here

  • Download and extract version 2.6 of htmlunit.  Download page can be found at http://sourceforge.net/projects/htmlunit/files/
  • Copy htmlunit/lib/*.jar into your .grails/{your grails version}/projects/{your project}/plugins/webtest-1.2.3/home/lib
  • Delete your old versions of htmlunit from the directory above

I create a new webtest

grails create-webtest MyTest

Create-webtest command

There is now a file called "MyTestTests.groovy" in your grails app.  It contains a method called "testMyTestListNewDelete" that you can nuke.  I deleted everything so that I was left with just the bare class:

class MyTestTests extends grails.util.WebTest {
}

 

 

 

Start firefox and install the web-recorder plugin.  While you can easily not use this plugin and just write the code yourself, it was helpful to me as I was getting started.  For simple tests, I would not use this any longer, but if I were going the next step, I wouldn't hestiate to fire up this plugin again.  Copy the groovy output of your test into your groovy test class.  Customize to your liking.

I had to comment out things that were generated from the recorder.  I *think* the double calls were the result of some behind-the-scenes ajax in the form.  For example, here I commented out the 2nd setInputField call.

setInputField(htmlId: "edit-taxonomy-tags-13", value: "University of Pittsburgh")
//setInputField(forLabel: "Undergraduate Institution: ", value: "University of Pittsburgh")

Here's a picture of the web-recorder in action:Web-Recorder in action

 

Now paste the groovy code that you obtain from the web-recorder into your test method.  This is my modified test (please ignore the commented-out lines for now, will explain these later).

import com.canoo.webtest.WebtestCase

class MyTestTests extends grails.util.WebTest {

    void testSomething() {
        webtest("check student status ") {
            invoke "http://localhost/"
            clickLink "Prospective Students"
            clickLink "Register now!"
            def uid = "philliprhode1";
           
            setInputField(name: "name", value: uid)
            //setInputField(htmlId: "edit-name", value: "webtest1")
            setInputField(name: "mail", value: uid + "@philliprhodes.com")
            //setInputField(htmlId: "edit-mail", value: "webtest1@philliprhodes.com")
            //pass[pass2]
            setInputField(description: "Set password field pass[pass1]: phillip123", name: "pass[pass1]", value: "phillip123")
            //setInputField(description: "Set password field pass[pass1]: phillip123", htmlId: "edit-pass-pass1", value: "phillip123")
            setInputField(description: "Set password field pass[pass2]: phillip123", name: "pass[pass2]", value: "phillip123")
            //setInputField(description: "Set password field pass[pass2]: phillip123", htmlId: "edit-pass-pass2", value: "phillip123")
            setInputField(name: "field_profile_first_name[0][value]", value: "phillip")
            setInputField(htmlId: "edit-field-profile-first-name-0-value", value: "phillip")
            setInputField(name: "field_profile_last_name[0][value]", value: "test")
            setInputField(htmlId: "edit-field-profile-last-name-0-value", value: "test")
            setInputField(name: "field_profile_undergrad_gradyear[0][value]", value: "1985")
            setInputField(htmlId: "edit-field-profile-undergrad-gradyear-0-value", value: "1985")
            setInputField(name: "taxonomy[tags][10]", value: "Pittsburgh")
            setInputField(htmlId: "edit-taxonomy-tags-10", value: "Pittsburgh")
            setInputField(name: "taxonomy[tags][12]", value: "PA")
            setInputField(htmlId: "edit-taxonomy-tags-12", value: "PA")
            setInputField(name: "taxonomy[tags][11]", value: "United States")
            setInputField(htmlId: "edit-taxonomy-tags-11", value: "United States")
            setInputField(name: "taxonomy[tags][13]", value: "University of Pittsburgh")
            setInputField(htmlId: "edit-taxonomy-tags-13", value: "University of Pittsburgh")
            //setInputField(forLabel: "Undergraduate Institution: ", value: "University of Pittsburgh")
            setInputField(name: "field_profile_lsac[0][value]", value: row.lsac_no)
            setInputField(htmlId: "edit-field-profile-lsac-0-value", value: row.lsac_no)
            //setInputField(forLabel: "LSAC Number: ", value: "L12345678")
            clickButton "Create new account"
            def check = row.stat
            verifyText(description: "Verify that text is contained in the page", check)
            clickLink "Sign Out"
        }

    }
}

 

After you have pasted in the output of the web-recorder into your test, run your test using the following:

grails test-app -functional

 

Hopefully you will see something like :
Welcome to Grails 1.1.1 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /Users/prhodes/local/grails/grails-1.1.1

Base Directory: /Users/prhodes/Documents/workspace-xx/testgl
Running script /Users/prhodes/local/grails/grails-1.1.1/scripts/TestApp.groovy
Environment set to test
    [mkdir] Created dir: /Users/prhodes/Documents/workspace-xx/testgl/test/reports/html
    [mkdir] Created dir: /Users/prhodes/Documents/workspace-xx/testgl/test/reports/plain

Starting functional tests ...

 

At the end of running your test, a browser will open up and let you know how it went:  If you had a failure, you can drill into the reports and actually see the html that was emitted from your Drupal application.

In my case, when I was running the test, I would get a failure that the Html element could not be found.  I suspect that the webrecorder was picking up some ajax events which wouldn't occur in my test, AND looking at the test code, I thought that the problem lines were duplicated in the line above, so I just commented out these lines.

Here's an example of one.  As you can see, I am setting my input field to a value of my school, and it's not required for this test to set the label for this field.  I think it's safe to comment this out.

            setInputField(htmlId: "edit-taxonomy-tags-13", value: "University of Pittsburgh")
            //setInputField(forLabel: "Undergraduate Institution: ", value: "University of Pittsburgh")

It took me a couple of runs to get my test to run correctly.

 

Now that we have a functional webtest, let's repeat implement data-driven behavior.  One note here, since this is a "WebTest" and not a Integration test, I could not just inject the datasource, so I have to create a datasource myself.  I found enough to get me going with Groovy/JDBC at http://www.ibm.com/developerworks/java/library/j-pg01115.html  To summarize, I perform a select of my status table, loop through the records and perform a webtest for each record in my table using parameters from the mysql table.  Here's my final file:

import groovy.sql.Sql

import com.canoo.webtest.WebtestCase

class MyTestTests extends grails.util.WebTest {
    void testSomething() {
        def sql = Sql.newInstance("jdbc:mysql://localhost:3306/authsum", "authsum", "authsum", "com.mysql.jdbc.Driver")
        sql.eachRow("select * from status where lsac_no != 'LSAC_Acct_No'", { row ->
                        runtest(row)
                        })
    }
   
    def runtest(def row) {
   
        println row.stat
        println row.lsac_no  
        webtest("check student status ") {
            invoke "http://localhost/"
            clickLink "Prospective Students"
            clickLink "Register now!"
            //you can increment the following and rerun the tests so that you can continually register new users
            //without getting a dup username error
            def uid = "test20" +  row.lsac_no;
           
            setInputField(name: "name", value: uid)
            //setInputField(htmlId: "edit-name", value: "webtest1")
            setInputField(name: "mail", value: uid + "@philliprhodes.com")
            //setInputField(htmlId: "edit-mail", value: "webtest1@philliprhodes.com")
            //pass[pass2]
            setInputField(description: "Set password field pass[pass1]: phillip123", name: "pass[pass1]", value: "phillip123")
            //setInputField(description: "Set password field pass[pass1]: phillip123", htmlId: "edit-pass-pass1", value: "phillip123")
            setInputField(description: "Set password field pass[pass2]: phillip123", name: "pass[pass2]", value: "phillip123")
            //setInputField(description: "Set password field pass[pass2]: phillip123", htmlId: "edit-pass-pass2", value: "phillip123")
            setInputField(name: "field_profile_first_name[0][value]", value: "phillip")
            setInputField(htmlId: "edit-field-profile-first-name-0-value", value: "phillip")
            setInputField(name: "field_profile_last_name[0][value]", value: "test")
            setInputField(htmlId: "edit-field-profile-last-name-0-value", value: "test")
            setInputField(name: "field_profile_undergrad_gradyear[0][value]", value: "1985")
            setInputField(htmlId: "edit-field-profile-undergrad-gradyear-0-value", value: "1985")
            setInputField(name: "taxonomy[tags][10]", value: "Pittsburgh")
            setInputField(htmlId: "edit-taxonomy-tags-10", value: "Pittsburgh")
            setInputField(name: "taxonomy[tags][12]", value: "PA")
            setInputField(htmlId: "edit-taxonomy-tags-12", value: "PA")
            setInputField(name: "taxonomy[tags][11]", value: "United States")
            setInputField(htmlId: "edit-taxonomy-tags-11", value: "United States")
            setInputField(name: "taxonomy[tags][13]", value: "University of Pittsburgh")
            setInputField(htmlId: "edit-taxonomy-tags-13", value: "University of Pittsburgh")
            //setInputField(forLabel: "Undergraduate Institution: ", value: "University of Pittsburgh")
            setInputField(name: "field_profile_lsac[0][value]", value: row.lsac_no)
            setInputField(htmlId: "edit-field-profile-lsac-0-value", value: row.lsac_no)
            //setInputField(forLabel: "LSAC Number: ", value: "L12345678")
            clickButton "Create new account"
            def check = row.stat
            verifyText(description: "Verify that text is contained in the page", check)
            clickLink "Sign Out"
        }

    }
}


I hope you found my write-up of data-driven testing of Drupal using the Grails application stack useful. Feel free to contact me if you find any errors or omissions related to my write-up at http://www.philiprhodes.com