After working on a GRAILS project, I decided to dig a little deeper into the underlying technology: Groovy, a 21st century language with a 70′s name . There are many good things that can be written about Groovy being a dynamic, interpreted language built on Java, but in this post I’ll focus on two things :
- How to access a bundled jar and all it’s dependencies from a Groovy script.
- How to add methods to classes in that bundled jar without modifying the jar.
What that means is that I can take any Java functionality that is bundled as a jar file, and surgically pick a class from that jumble, modify it and run it in a scripted environment without having to write another bootstrap application! Let us see a real-world use of this feature.
The problem to solve: At a certain client’s site, there is a flurry of activity every 4 months when release time comes around. The company’s flagship application is installed at 100+ client sites but along with the application are installed file system resources and database resources. And even though there are automated tools to carry out this activity, once done, every install is slightly different because of environmental reasons. So to really make sure that all went well, programmers have to do the unthinkable… log into each individual site and putz around till they are sure that the installation looks good. And do that a 100 times! That, is what I call a PROBLEM DOMAIN! Although, we will not be using Groovy to create a DSL to solve this problem (next post), I will demonstrate some of Groovy’s grooviest features to alleviate the problem.
What already existed
The flagship application already had an integration test framework. As a separate maven module, the integration-test project had a pom in which dependencies were declared with all the other modules that were being integration tested. The maven powered integration test environment fired up a jetty server and a Selenium server in the pre-integration-test phase ran the Selenium JUnit tests and then stopped both the servers in the post-integration-test phase. The results of the tests were available via the maven-surefire-plugin reporting mechanism. The packaging of this module was of type jar.When packaged or installed, the jar file produced contained all the Selenium based JUnit tests inside of it.
What would be nice to have
Since we already have a jar with all Selenium tests written for one site, it would be great if I could re-use that code for each of the 100 sites that have to be sanity checked every 4 months. However, the URLs and credentials for running the tests are read from property files that are a part of the code base. It would be great if we could pass in new URLs and credentials to those tests and yet not break the Integration tests that run with nightly builds.
Also, it would be great if we could access classes from that jar in a Groovy Script without having to write yet another maven module with dependencies and the whole nine yards.
So here’s a Groovy Solution!
In Groovy we can either write a script or a class. Although both end up being compiled into a .class file, I find writing a script file more liberating (or ad-hoc) so that’s what I used.
First let’s tackle the issue of using a class from any arbitrary jar file in my Groovy script.
The initial thought is to place that jar file in
<GROOVY_HOME>/lib or to specify it on the command line with the -cp flag like so:
>groovy -cp ~/jars/arbitrary.jar MyScript.groovy
However, it is more than likely that arbitrary.jar doesn’t play by itself and lives in an jar-soup of some sort with tons of collaborators and dependencies to do it’s work (see figure). So the classpath option is no good. We will need to make certain assumptions about the jar file:
- Arbitrary.jar is available in a Maven repository (local or remote)
- Arbitrary.jar has dependencies that are specified in a pom.xml buried inside it’s META-INF directory OR that it has no dependencies and has all the classes it needs to run bundled within the jar.
If both of the above are true, then the solution of the above problem can be solved by using a nifty tool from the Groovy camp called Grape. Grape allows you to Grab dependencies by using an annotation right above your import statement (yes, you can have imports in a groovy script). And in keeping with scripting’s ad-hoc nature, no xml files for specifying dependencies, thank God!
@Grab (group='com.acme.widgets', module='sales-integration-test', version='1.2.0') import com.acme.test.SmokeTests
By doing the above, you have loaded that artifact (jar file) into the GroovyClassLoader that is used by the script.
Grape uses Ivy under the covers. So a little primer on Ivy may be in order. Ivy uses a config file (an xml file this time ) that tells it where to resolve it’s dependencies from. Since Grape is using Ivy, Grape is configured to look for that ivy config in <USER_HOME>/.groovy/grapeConfig.xml. Here is what it looks like:
<ivysettings> <settings defaultResolver="downloadGrapes"/> <resolvers> <chain name="downloadGrapes"> <filesystem name="cachedGrapes"> <ivy pattern="${user.home}/.groovy/grapes/[organisation]/[module]/ivy-[revision].xml"/> <artifact pattern="${user.home}/.groovy/grapes/[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]"/> </filesystem> <ibiblio name="local" root="file:/usr/jay/maven2repo" m2compatible="true"/> <ibiblio name="local2" root="http://nexus.acme.com:9000/nexus/repository/repoGroup/" m2compatible="true"/> <ibiblio name="codehaus" root="http://repository.codehaus.org/" m2compatible="true"/> <ibiblio name="ibiblio" m2compatible="true"/> <ibiblio name="java.net2" root="http://download.java.net/maven/2/" m2compatible="true"/> </chain> </resolvers> </ivysettings>
The things to note here:
- “local” tells Ivy to look first in the local maven repository (optional). “local2″ tells Ivy to look in the company configured maven artifact repository. Also optional. And lastly to go out on the net and get the artifact from remote repositories.
- “cachedGrapes” tells Ivy where to store the downloaded artifacts and in what format.
So when the import statement in the Groovy script looks for SmokeTests , not finding it in the Groovy ClassLoader, it looks at the annotation on the import. The @Grab annotation specifies the coordinates of the artifact which is resolved using the resolvers specified in the ivysettings file. The downside to this approach is that we end up with two local caches, one for Maven and another for Ivy. But the good thing is that the @Grab annotation passes the resolved artifacts directly to the Groovy ClassLoader so now SmokeTests is available directly in the script.
I must point out that I had a classpath problem with slf4j classes (that were dependencies in the jar). I used a sledgehammer approach by making ALL classes bump up to the SystemClassLoader by specifying the following:
@Grapes ([ @Grab (group='com.acme.widgets', module='sales-integration-test', version='1.2.0'), @GrabConfig(systemClassLoader=true) ]) import com.acme.tests.SmokeTests
So that takes care of the dependency issue.
–
The other issue we had identified was that of needing to change some code on the fly without modifying the jar file.
Let’s begin by looking at the existing JUnit code that exists in the jar file.
public class SmokeTests { private static String url = PropertyUtils.getPropertyFromFile("url", "test.properties"); private static String context = PropertyUtils.getPropertyFromFile("context", "test.properties"); private static String username = PropertyUtils.getPropertyFromFile("user", "test.properties"); private static String password = PropertyUtils.getPropertyFromFile("password", "test.properties"); ... @Test public void testLogin() throws Exception { try { selenium.open(context + "/login.jsp"); selenium.type("j_username", username); selenium.type("j_password", password); selenium.click("login"); selenium.waitForPageToLoad("10000"); String xpathToCustomerTab = "//*[@id='span4']"; SeleniumUtils.clickWhenElementLoads(selenium, xpathToCustomerTab); String xPathToLogoutLink = "//html/body/div[3]/a[4]"; SeleniumUtils.clickWhenElementLoads(selenium, xPathToLogoutLink); } catch (ElementNotFoundException e){ Assert.fail(e.getMessage()); } }
Listing 1 – Original JUnit Test Class
In the above code we see how test.properties are being read for the context, url and credentials. We need to be able to change those values by passing them externally and yet not causing the Integration tests to break. Here is where we will use a trick from the Groovy meta-programming toolkit called ExpandoMetaClass (or EMC).
In my Groovy Script, I will create an instance of SmokeTests, get at it’s metaClass that Groovy wraps around every Java and Groovy class and add a method to it. That method will be available for the scope of the script. Here’s how I would do it:
@Grapes ([ @Grab (group='com.acme.widgets', module='sales-integration-test', version='1.2.0'), @GrabConfig(systemClassLoader=true) ]) import com.acme.tests.SmokeTests def smokeTests = new SmokeTests() smokeTests.metaClass.changeStuff = {url, context, userId, password -> delegate.url = url delegate.context = context delegate.username = userId delegate.password = password } ... //Read url, credentials from file: //For each line loop smokeTests.changeStuff (url, context, userId, password) //change the values on this instance smokeTests.testLogin() //call the original test on the class //EndLoop
Listing 2 – Groovy script file
By doing the above, I’ve been able to interject new values for those private static member variables for the duration of the script, all without changing the original class.
Conclusion
We have seen how Groovy can help in ad-hoc scripting by being able to access a class(es) from a packaged jar and we have seen how we can change the functionality of existing classes. I have used the use case of Testing to illustrate this but this technique can be used in any situation where existing behavior has to be quickly modified.
In the next post, I will talk about using Groovy for creating a DSL for integration testing.
Resources:
http://www.ibm.com/developerworks/java/library/j-pg06239/index.html
http://jira.codehaus.org/browse/GROOVY-3755?page=com.atlassian.jira.plugin.system.issuetabpanels%3Aall-tabpanel#issue-tabs
http://mrhaki.blogspot.com/2010/04/groovy-goodness-configuring-grape-to.html
private static final String CONTEXT = “/acme-widgets” ;
public static void loginPage(Selenium selenium, String userId, String password) throws SVElementNotFoundException{
selenium.open(CONTEXT + “/login.jsp”);
selenium.type(“j_username”, userId);
selenium.type(“j_password”, password);
selenium.click(“login”);
selenium.waitForPageToLoad(“30000″);
String xpathToDataManagementTab = “//*[@id='span7']“;
waitToProcess(selenium, xpathToDataManagementTab, 60);
selenium.click(xpathToDataManagementTab);
String xPathToLogoutLink = “//html/body/div/div[3]/a[4]“;
waitToProcess(selenium, xpathToDataManagementTab, 60);
selenium.click(xpathToDataManagementTab);
}