Introduction
In this series of blogs I'm going to
- introduce the Concordion framework - a simple yet powerful way to write presentable, durable automated acceptance tests that run on the JVM
- show how those Concordion tests can be written in Groovy and used to drive the Selenium testing API and test a Grails web application. (Why Grails? Well it could be any web app stack but Grails is what I'm currently using in my projects and I think it rocks in terms of productivity!)
- show how easy it is to use Maven2 to run the whole thing in a CI compatible set of pom files
Full code examples will be attached for you to download and try! I hope you enjoy them...
Part2 - Automation with Selenium and Maven2
| Source Code Full Source code for this part is available here. |
| Source Code Part 1 of this series is available here |
In part 1 I introduced the Concordion framework and an example requirement/feature where we wanted to be able to save books in our application.
But the following tasks were still outstanding
- generating a Grails bookstore app for testing
- using the Selenium API from our Concordion test to test the Grails application
- using maven2 to control the whole process
In this post we're going to finish these remaining tasks and get Concordion driving a real test with Selenium and maven2.
Generating a Grails bookstore app for testing
This has got to be the easiest part. We can use the maven2 support in Grails to generate a new application from scratch.
Within our existing project structure go into the concordion-example-parent directory and type
mvn org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-4:generate -DarchetypeGroupId=org.grails -DarchetypeArtifactId=grails-maven-archetype -DarchetypeVersion=1.1 -DgroupId=com.dish2dish.concordion.examples -DartifactId=concordion-example-web
This will generate a concordion-example-web application sub directory. Navigate into that sub directory now and type
mvn initialize
This will set up the project fully as a grails application.
The only remaining actions are to add our existing parent pom as the web project's parent
...
<parent>
<artifactId>concordion-example-parent</artifactId>
<groupId>com.dish2dish.concordion.examples</groupId>
<version>0.1-SNAPSHOT</version>
</parent>
...
and conversely add this new project as a maven module in the parent pom.
Once this is done we can turn our attention to generating some behaviour in our Grails web application.
Using Grails domain objects and scaffolding
We're going to take advantage of a fantastic prototyping feature in Grails called scaffolding. Here's what we want to do from our existing Concordion Spec and test case -
Create a Bookstore application that let's us create named Books
So we are going to need a Book class with a name property. In Grails we can do this quickly and easily -
Go into the concordion-example-web directory on the command line and type
mvn grails:create-domain-class com.dish2dish.concordion.examples.Book
This will generate a domain class called Book in the concordion-example-web\grails-app\domain directory. Navigate to it in your favourite IDE and edit it so it looks like this
package com.dish2dish.concordion.examples class Book { String name static constraints = { name(blank: false) } }
In Grails you can provide properties on domain classes which will have getters/setters generated at runtime. In our case we've just provided one property name. By default all properties on Grails domain classes are persistent and mapped using Hibernate behind the scenes.
We've just got one more task in order to finish our bookstore app.
Go into the concordion-example-web\grails-app\controllers\com\dish2dish\concordion\examples directory and create a new file called BookController.groovy which should look like this
package com.dish2dish.concordion.examples
class BookController {
def scaffold = Book
}
This controller class will instruct Grails to create scaffolded views at runtime to enable us to perform all CRUD operations on our Book domain class. In short we will have a web application that will let us create books and fulfil our requirement.
You should now be able to navigate to the concordion-example-parent\concordion-example-web directory on the command line and type
mvn grails:run-app
This will run up your grails application at http://localhost:8080/concordion-example-web where you can play around with it and see what is available for such little code with Grails!
Using the Selenium API from our Concordion test to test the Grails application
So we have a Grails application, but this was a means to an end - testing with Concordion and Selenium. In real life though your development team could have followed these steps exactly.
- Create a Concordion Spec as a team
- Developer writes implementation
The next logical step is for the tester to manually test the implementation and then automate that test with the developer. One way to do this is to have the tester record their test using Selenium IDE. I won't go into details of how to use Selenium IDE as the existing documentation is excellent http://seleniumhq.org/docs/
Selenium IDE offers a way to save a recorded test as a Groovy test which is what we'll start with. Record a test in Selenium IDE where you navigate to the create book controller link and then create a new book called Moby Dick.
Then export the test case as a Groovy Test script and save it to
concordion-example-parent\concordion-example-integration-tests\src\test\groovy\com\dish2dish\concordion\examples\BookScript.groovy with our other integration test files.
Although that recorded Selenium IDE test case is a good start, we're only really interested in Selenium IDE's ability to pick out the right ids and commands for us. We really just want to use the Selenium API like this
package com.dish2dish.concordion.examples import com.thoughtworks.selenium.Selenium import com.thoughtworks.selenium.GroovySeleneseTestCase import com.thoughtworks.selenium.DefaultSelenium class BookScript extends GroovySeleneseTestCase { DefaultSelenium selenium void prepareToCreateBook() { selenium.open("/concordion-example-web/book/list") selenium.click("link=New Book") waitFor { selenium.isTextPresent("Create Book") } } String saveBookWithName(String bookName) { selenium.type("name", bookName) selenium.click("//input[@value='Create']") String savedBookName = "" waitFor { savedBookName = getBookName(selenium) } savedBookName } private String getBookName(Selenium selenium) { return selenium.getTable("//table.1.1") } }
There are a few key points here - the Selenium IDE test has been rewritten as a script class which just uses the Selenium API to perform certain operations on the UI. It expects to be injected with a selenium instance at runtime to do this.
This script has a prepareToCreateBook method which will drive the UI to the appropriate page where a book can be created.
It also has a saveBookWithName method which takes in the name of a book to be created. The script then uses the Selenium API to drive the necessary UI actions to the save the book and then extract the saved book's name which is returned to the caller.
So how will we use this script? Well we need to edit our existing BookTest Groovy class from part1 to use our new script.
Edit the BookTest.groovy file so it looks like this
package com.dish2dish.concordion.examples import org.concordion.integration.junit4.ConcordionRunner import org.junit.runner.RunWith import org.junit.BeforeClass import com.thoughtworks.selenium.DefaultSelenium import org.junit.AfterClass @RunWith (ConcordionRunner.class) class BookTest { static BookScript bookScript static DefaultSelenium selenium @BeforeClass public static void beforeAll() { selenium = new DefaultSelenium("localhost", 4444, "*chrome", "http://localhost:8080"); selenium.start(); bookScript = new BookScript(selenium: selenium); } @AfterClass public static void afterAll() { selenium.stop(); } void prepareToCreateBook() { bookScript.prepareToCreateBook() } Map createBook(String bookName) { String savedBook = bookScript.saveBookWithName(bookName) [name: savedBook] } }
We've changed our test so it is initialising a DefaultSelenium object instance and also an instance of our BookScript class. We've also changed the prepareToCreateBook method to use the script implementation.
Finally we've changed the createBook method to use our script also, which should use the bookName provided by Concordion and use it to try and create a book via the front end. We also take the response from our script class and pass it back to Concordion so it can be checked in a Concordion level assertion.
For the integration test project to compile we need to update the pom to include the selenium api dependency also.
...
<dependency>
<groupId>org.seleniumhq.selenium.client-drivers</groupId>
<artifactId>selenium-java-client-driver</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
...
Once that's done we can try and run our test in our IDE.
As part of the selenium api being downloaded you should also get hold of the selenium server jar. We'll get it shortly as part of the selenium maven plugin but you can download it from here for now.
Once you've got this jar you can simply double click if you have Java 1.5+ installed and it will launch the selenium server. Selenium server acts as a proxy on to your application and must be running in order for the Selenium API to do it's thing.
Alternatively you can easily run the server with java -jar selenium-server-1.0.1-standalone.jar from the command line.
Once the server is up and running you should be able to run the Concordion test within your IDE to test all is good.
Automating everything for CI in maven2
This is all well and good, but the real key to running Concordion tests or any automated tests for that matter is running them often. This is best done within a Continuous Integration (CI) environment and needs to be scripted.
Now a lot of people don't like maven2 but here's one of the reasons it can sometimes really kick butt. The maven2 plugins. Today in maven2 I can automate this whole thing thanks to maven2 plugin support. It was quite shockingly easy (although I have to admit just a couple of years ago this would have been painful) which shows in this specific case the plugin community has come a long way.
To automate this process we need to be able to do a few things.
- Start and stop the selenium server
- Start and stop our web application
- Run our Concordion test against the whole thing
Sounds tricky but with maven2 plugin support this is pretty easy.
The following snippet adds in Selenium server integration
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>selenium-maven-plugin</artifactId>
<version>1.0-rc-1</version>
<executions>
<execution>
<id>start</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start-server</goal>
</goals>
<configuration>
<background>true</background>
<logOutput>true</logOutput>
</configuration>
</execution>
<execution>
<id>stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop-server</goal>
</goals>
</execution>
</executions>
</plugin>
and the following snippet will ensure that our web application is deployed and running in jetty prior to our tests running
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>1.0</version>
<configuration>
<container>
<containerId>jetty6x</containerId>
<type>embedded</type>
</container>
<wait>false</wait>
<configuration>
<properties>
<cargo.servlet.port>7001</cargo.servlet.port>
</properties>
<deployables>
<deployable>
<groupId>com.dish2dish.concordion.examples</groupId>
<artifactId>concordion-example-web</artifactId>
<type>war</type>
<properties>
<context>concordion-example-web</context>
</properties>
</deployable>
</deployables>
</configuration>
</configuration>
<executions>
<execution>
<id>start-container</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>stop-container</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
</plugin>
One thing to notice is that I run up jetty on port 7001. Therefore we must make sure that our test case is changed so our DefaultSelenium instance also starts on port 7001.
@BeforeClass
public static void beforeAll() {
selenium = new DefaultSelenium("localhost", 4444, "*chrome", "http://localhost:7001");//set port to 7001
selenium.start();
bookScript = new BookScript(selenium: selenium);
}
Once these changes are made you should be able to run mvn clean install from the parent project and see everything being built and then tested by the integration test project.
Conclusion
Concordion is a great framework for writing tests that are easy to read by the whole team. It can also be run easily within an IDE unlike tools like Fitnesse.
Concordion can be used to separate testing out into 2 distinct phases
- specification
- script
This allows requirements to be defined early which are intention based and more durable as a result. Once the specs have some initial prototypes and/or have been coded up with a stable user interface the specification can be bound to a script via the Concordion instrumentation mechanism.
In our example the scripts are written in Groovy and use the Selenium API to test a Grails application. Grails allowed us to create a demo application very quickly for our purposes, but can be used to take prototypes very quickly on to production quality apps.
If you set up your applications to run within maven2 there are a number of excellent plugins that let you test your application with Concordion very easily.
I have been using this technique to test web applications under CI for the past 6 months and it is working very nicely. I am about to start using it on our first Grails application.
I hope this will help you towards ATDD in your future projects!
