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






 

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

In honour of Chicago's annual

In honour of Chicago's annual Marathon, Nike has hooked up Chi-town with their very own Air Max Lunar 1 WR, dressed in the signature coach wallets on sale 'Chicago Bears' colourway. The orange and blue contrast against the black upper, with coach factory outlet small detailing on the heel, lace tips and the insole reads 'Chicago Marathon'. orange nike shox Team Swoosh have made sure these Lunarlon kicks are ready for any weather, nike air max 1 they are water resistant and sport a pressure fused mesh upper. The marathon louis vuitton outlet was last weekend, so unfortunately these have already sold out from Nike.com but louis vuitton purses on sale scope out eBay for any pairs floating around.There's a ton of stores coach outlet online that sell sneakers and give a passing thought to the accessories we need cheap nike air max to keep our kicks looking fresh, but we can't think of many places coach that focus just on those extra bits. Sneaker Science is a new site nike air max from the UK that does just that though, offering up a stack of air max ltd cleaning and protection products, paints and storage solutions, laces and more. They also womens nike free sell Sneaker Freaker, wise move guys! If you're in need of any of nike outlet stores in ohio the aforementioned gear, check out Sneaker Science here.German court master Boris Becker louis vuitton outlet online would take home almost 50 titles in his glittering career, and back in cheap coach purse 1985 at Wimbledon, the writing was on the green. The 17-year-old came into the tournament unseeded louis vuitton outlet stores and left with the championship trophy. He did so wearing a pair of Pumas, and now the big cat brand are bringing the signature pair back through the Puma Select program nike air max griffey to premium CREAM stockists. They'll be rolling into stores soon, but watch the http://www.officiallouisvuittononlinestore.cc video below to get in the mood, it features Boris and his son nike outlet online store Noah talking us through the famous win.Looks like the Nike designers needed coach outlet a break from all the triple black releases and decided to vent with louis vuitton online this very Volt Air Force 1. Hat's off to you if you think nike air max 2015 you can pull this number off��.available now from Kinetics in Japan.For lovers nike air max 95 of vintage adidas Originals sneakers, the promised land does indeed exist, and it's nike total air foamposite max in Argentina. A few months ago, a band of Three Strips fiends traveled coach outlet online store to Buenos Aires after a tip-off that there was a treasure trove of louis vuitton stores untouched vintage heat hiding in a suburban shoe shop. It gets better, one louis vuitton outlet store locations of the pilgrims says what they found was, 'Beyond our wildest dreams', with louis vuitton reams of ancient rare and forgotten sneaks stacked in a labyrinthine storage room coach factory along with the humble shop front. Watch footage from the trip in the nike air max 90 video below, but for the full story, grab the latest issue of Sneaker nike shoes free shipping Freaker, we've got a massive photo feature accompanied by an interview with sneaker coach Wristlets trekker Gary Aspden.
related:
air jordan
nike foamposite
jordan chris paul

ah: about filling the

ah: about filling the database, check "dbmonster".. a cool framework for that purpose.

if you think Java is

if you think Java is complicated, few chances:

1) yu don't use Java for a long time..
2) you never used it properly ... :)
3) you definitelly don't like Java and you are desperatelly looking for an argument to publicize that out there :) eheh

* btw, grails is cool ... but for the scenario you described, Java would do the same job with the same effort..

skeptical?

1) install Maven
2) mvn archetype:generate -DarchetypeCatalog=http://download.java.net/maven/2
3) chosse option 2 and answer the 5 questions with foo data
5) mvn clean compile
6) mvn glassfish:run

Done, a web application will be available in your browser:
http://localhost:8080/artifactId/webresources/myresource

now it is time to edit the source files and include your test code.. with full debug and support of the major IDEs...

Hard work? much harder than grails? really?

my 2 Java cents.

I will have to agree to

I will have to agree to disagree with your post on 11/23/2009 at 04:07

Java would do the same job, but with EXTRA effort, not the same?!

The steps you describe may produce a helloworld j2ee web application, but that is not what Phillip needs here.

What he needs is a fast way to setup and automate (via data) integration testing...to do that is very fast and easy using grails, as Phillip has so well explained here

To do the same in java, you will have to do quite a bit more hand-holding to get it working.

Speaking as a developer who has used webtest to perform testing in both J2EE and Grails, I can vouch grails is ALOT easier to setup, hence your comments (in my opinion and experience) do not hold true.

Hope that helps others when reading this blog entry.
Conor

The described Maven approach

The described Maven approach is simply setting up the necessary build structure. Sure you can add dependencies, integrate the project into your IDE, and you're off to the races. But, again, Grails was ideally suited for this specific case of webtesting because:

  1. testing convention
  2. dynamic nature with facilitation of Groovy + Java
  3. Webtest plugin integration

+1 @Conor

Filling database with right

Filling database with right data is essential for complex functional testing. Basically without database fixtures it is impossible.

I was thinking a lot about implementing functional testing and grails looks like great tool to go with.

Thanks for pointing out! Nice post, easy to read :)

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Post new comment

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
14 + 1 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.