To conclude the ParkCalc mini-series, I choose to work through test automation using keywords with FitNesse. As I was using Selenium mostly with RobotFramework, I decided to use Selenesse for the integration into the FitNesse environment. Here is the write-up as I implement the tests.
Getting started
First off, I decided to move the existing tests so far into a RobotFramework folder and create a separate FitNesse folder. In order to get started with FitNesse, I downloaded the latest FitNesse version together with SeleniumRC and the Selenesse library. Here is the directory structure after downloading everything I need:
fitnesse.jar
lib/selenium-java-client-driver.jar
lib/selenium-server.jar
lib/selenesse.jar
Now, I start FitNesse in order to prepare the folder structure.
java -jar fitnesse.jar
You may optionally specify a port if the default is used on your computer. But I’ll leave the explanations on how to use fitnesse to the user documentation, just as I did with RobotFramework. In addition you may want to take a closer look on the FitNesse tutorials which Brett Schuchert wrote about one year back, but which still should be up-to-date.
A first test
The initial folder structure can be found here. In the Makefile I provided that is an acceptance_tests target, which trigger FitNesse to run the ParkCalcSuite. This suite is not there, so we’ll create it first. Therefore I open my brower on localhost:80/ParkCalcSuite and extend the default page by defining the TEST_SYSTEM variable to point to slim. This means that FitNesse will use Slim for the tests I’m going to create.
A [[!-ParkCalc-!][http://adam.goucher.ca/ParkCalc]] test suite using Slim.
----
!define TEST_SYSTEM {slim}
!*< Classpath setup
!path lib/*.jar
*!
!contents -R2 -g -p -f -h
Now we create a test page for the first test. As I will continue working in test suites for each parking lot, I create the first test page for Economy tests. Here is a first page content that drafts the tests for one parking hour:
!|import |
|selenesse |
|org.openqa.selenium.server|
!|script|SeleniumServer|
|boot |
!define URL {http://adam.goucher.ca/parkcalc/}
!define BROWSER {firefox}
!define DELAY {0}
!define PAGE_TITLE {Parking Calculator}
!|script |SlimSeleniumDriver|localhost|4444|${BROWSER}|${URL}|
|open |${URL} |
|set speed|${DELAY} |
|check |get title |${PAGE_TITLE} |
!|script |
|one hour|Economy Parking|costs|$ 2.00|
!|script |
|shut down selenium server|
The test is split into several parts. The first table defines imports. Then the selenium server is started in the second table. After defining some variables the slim selenium driver from Selenesse is initialized. We will put the tables up until this point into a separate SuiteSetUp shortly. The next table define the actual test. The last table on the page states to stop the SeleniumServer again, thereby tearing down the test.
We need to set the test property on this first test page, as I called it EconomyTests. When we hit the test button in the browser, we see selenium opening, and loading the page, but nothing is entered. We’re going to fix this by creating a scenario table thereby defining the keyword one hour parking costs. Here is the scenario table that does the job:
!define PAGE_LOAD_TIMEOUT {5000}
!define COST_ITEM {xpath=//tr[td/div[@class='SubHead'] = 'COST']/td/span/font/b}
!|scenario |one hour |parkingLot |costs |costs |
|select |Lot | |@parkingLot |
|type |EntryTime | |12:00 |
|make checked |xpath=//input[@name='EntryTimeAMPM' and @value='AM']|
|type |EntryDate | |05/04/2010 |
|type |ExitTime | |12:59 |
|make checked |xpath=//input[@name='ExitTimeAMPM' and @value='AM'] |
|type |ExitDate | |05/04/2010 |
|click |Submit |
|wait for page to load|${PAGE_LOAD_TIMEOUT} |
|$ACTUAL_COSTS= |get text |${COST_ITEM} |
|check |echo |$ACTUAL_COSTS | |@costs |
In order to use the echo support, I need to add the following just below the imports:
!|import |
|selenesse |
|org.openqa.selenium.server|
|fitnesse.slim.test |
!|Library |
|EchoScript|
The scenario table defines the steps necessary to execute the test against the page. First the parking lot is selected, then the date and time values for the entry and exit dates are entered, the submit button is pressed and in the end the actual costs are taken from the page and compared to the expected values. The scenario is pretty awkward in its current shape, but we’ll get to refactor this in a moment. After saving the page, we hit the test button, and see that the test is failing. When we expend the section with the scenario, we see that the page offered $4 as parking rate, while we expected $2. So, we may now submit what we have before slicing out the parts in order to prepare for the other tests.
You may inspect the source tree up to this point here.
Some structure
Now, let’s give this page some love. We’ll organize the test into a SuiteSetUp, a SuiteTearDown, and a Scenario Library. These are mechanisms that FitNesse makes use of when running the tests. Let’s start with extracting the SuiteSetUp:
!|import |
|selenesse |
|org.openqa.selenium.server|
|fitnesse.slim.test |
!|Library |
|EchoScript|
!|script|SeleniumServer|
|boot |
!define URL {http://adam.goucher.ca/parkcalc/}
!define BROWSER {firefox}
!define DELAY {0}
!define PAGE_TITLE {Parking Calculator}
!|script |SlimSeleniumDriver|localhost|4444|${BROWSER}|${URL}|
|open |${URL} |
|set speed|${DELAY} |
|check |get title |${PAGE_TITLE} |
This is the SuiteTearDown:
!|script |
|shut down selenium server|
And finally this is the library containing the scenarios for our tests:
!define PAGE_LOAD_TIMEOUT {5000}
!define COST_ITEM {xpath=//tr[td/div[@class='SubHead'] = 'COST']/td/span/font/b}
!|scenario |one hour |parkingLot |costs |costs |
|select |Lot | |@parkingLot |
|type |EntryTime | |12:00 |
|make checked |xpath=//input[@name='EntryTimeAMPM' and @value='AM']|
|type |EntryDate | |05/04/2010 |
|type |ExitTime | |12:59 |
|make checked |xpath=//input[@name='ExitTimeAMPM' and @value='AM'] |
|type |ExitDate | |05/04/2010 |
|click |Submit |
|wait for page to load|${PAGE_LOAD_TIMEOUT} |
|$ACTUAL_COSTS= |get text |${COST_ITEM} |
|check |echo |$ACTUAL_COSTS | |@costs |
This leaves us with the following for our Economy Tests:
!|script |
|one hour|Economy Parking|costs|$ 2.00|
Pretty crisp right now. When we execute the test, we see that it still fails for the same reason. We check in our modifications. Here‘s the link to the source tree at this point.
Extract method until you drop
Taking a closer look on the scenario library, I still don’t feel done with refactoring. Applying a technique that Uncle Bob Martin calls “extract method until you drop”, we slice down the scripted keyword to parts that are more handy. We work from the bottom up. The last two lines verify the actual costs. So we define a new scenario for that:
!define COST_ITEM {xpath=//tr[td/div[@class='SubHead'] = 'COST']/td/span/font/b}
!|scenario |verify costs|costs |
|$ACTUAL_COSTS=|get text |${COST_ITEM} |
|check |echo |$ACTUAL_COSTS||@costs|
The next two lines on our way to the scenario top indicate that we click the submit button and wait for the page to load. We extract these to their own scenario table as well.
!define PAGE_LOAD_TIMEOUT {5000}
!|scenario |click|button|and wait for page to load|
|click |@button |
|wait for page to load|${PAGE_LOAD_TIMEOUT} |
The next three lines in the scenario cover the exit time and date, the three above that the entry time and date. As before, we combine these into their own keywords.
!|scenario |enter entry date |date |and time |time | |ampm |
|type |EntryTime | |@time |
|make checked|xpath=//input[@name='EntryTimeAMPM' and @value='@ampm']|
|type |EntryDate | |@date |
!|scenario |enter exit date |date |and time |time | |ampm |
|type |ExitTime | |@time |
|make checked|xpath=//input[@name='ExitTimeAMPM' and @value='@ampm']|
|type |ExitDate | |@date |
The first line could be left as is, but right now it’s not at the same level of abstraction as the remaining keywords. Therefore I extract the simple statement into it’s own keyword as well.
!|scenario|select parking lot|parkingLot |
|select |Lot ||@parkingLot|
Here is the final one hour costs keyword after this step:
!|scenario |one hour |parkingLot |costs |costs |
|select parking lot|@parkingLot |
|enter entry date |05/04/2010|and time |12:00 | |AM |
|enter exit date |05/04/2010|and time |12:59 | |AM |
|click |Submit |and wait for page to load|
|verify costs |@costs |
This could be quite sufficient, but I would like refactor out another keyword for the whole chain taking the entry date and time, the exit date and time as well as the parking lot and expected costs, so that in subsequent tests I am able to describe keywords such as two hours, three hours, etc. using this keyword.
!|scenario |parking in|parkingLot|from |entryDate||entryTime||entryAmPm|until|exitDate||exitTime||exitAmPm|costs|costs|
|select parking lot|@parkingLot |
|enter entry date |@entryDate|and time |@entryTime| |@entryAmPm |
|enter exit date |@exitDate |and time |@exitTime | |@exitAmPm |
|click |Submit |and wait for page to load |
|verify costs |@costs |
Here is the refactored one hour costs keyword:
!|scenario |one hour |parkingLot|costs |costs |
|parking in|@parkingLot|from |05/04/2010||12:00||AM|until|05/04/2010||12:59||AM|costs|@costs|
After seeing this test still failing for the same reason, I check in the changed ScenarioLibrary. Here is the source tree up to this point.
Adding more tests
Now, I can add more tests for varying durations for the economy parking lot. Here are the final economy tests I created:
!define PARKING_LOT {Economy Parking}
!|script |
|one hour |${PARKING_LOT}|costs|$ 2.00 |
|four hours |${PARKING_LOT}|cost |$ 8.00 |
|five hours |${PARKING_LOT}|cost |$ 9.00 |
|first day |${PARKING_LOT}|costs|$ 9.00 |
|four days |${PARKING_LOT}|cost |$ 36.00 |
|first week |${PARKING_LOT}|costs|$ 54.00 |
|three weeks|${PARKING_LOT}|cost |$ 162.00|
And here are the additions to the scenario library accordingly:
!|scenario|four hours |parkingLot|cost |costs|
|parking in|@parkingLot|from |05/04/2010||12:00||AM|until|05/04/2010||04:00||AM|costs|@costs|
!|scenario|five hours |parkingLot|cost |costs|
|parking in|@parkingLot|from |05/04/2010||12:00||AM|until|05/04/2010||05:00||AM|costs|@costs|
!|scenario|first day |parkingLot|costs|costs|
|parking in|@parkingLot|from |05/04/2010||12:00||AM|until|05/05/2010||12:00||AM|costs|@costs|
!|scenario|four days |parkingLot|cost |costs|
|parking in|@parkingLot|from |05/04/2010||12:00||AM|until|05/08/2010||12:00||AM|costs|@costs|
!|scenario|first week |parkingLot|costs|costs|
|parking in|@parkingLot|from |05/04/2010||12:00||AM|until|05/11/2010||12:59||AM|costs|@costs|
!|scenario|three weeks|parkingLot|cost |costs|
|parking in|@parkingLot|from |05/04/2010||12:00||AM|until|05/25/2010||12:59||AM|costs|@costs|
After running the tests, I can see that the tests execute fine (with some problems in the error reporting), and that I may therefore check in the results. There resulting tree is here.
After having this first test, I can continue adding more tests in the same manner as before. The resulting tests can be found here.
Comparing the RobotFramework the most significant difference is that the tests are created and maintained on a web-server. Overall the reports and the tests differ to some degree, but they are quite similar to each for me. Both got expandable sections where the errors lurk in, but got a mechanism to extract keywords into more fine-grained keywords or to define variables and override them. I hope I showed that the discussion that most often flourish around which tool to pick for test automation is irrelevant. Agile test automation and acceptance test-driven development is about the approach, not the tool.
That’s it for the ParkCalc automation series so far. In case you got an idea how to continue this, please drop me a line, or just do it and send me a trackback. I hope you enjoyed it.
well done, sir.