Building and using an Object Repository in Selenium Webdriver

One of the main burdens of automated GUI test script maintainability is the amount of maintenance needed when object properties change within the application under test. A very common way of minimizing the time it takes to update your automated test scripts is the use of a central object repository (or object map as it’s also referred to sometimes). A basic object repository can be implemented as a collection of key-value pairs, with the key being a logical name identifying the object and the value containing unique objects properties used to identify the object on a screen.

Selenium Webdriver offers no object repository implementation by default. However, implementing and using a basic object repository is pretty straightforward. In this article, I will show you how to do it and how to lighten the burden of test script maintenance in this way.

Note that all code samples below are written in Java. However, the object repository concept as explained here can be used with your language of choice just as easily.

Creating the object repository
First, we are going to create a basic object repository and fill it with some objects that we will use in our test script. In this article, I am going to model a very basic scenario: go to the Bing search engine, search for a particular search query and determine the number of search results returned by Bing. To execute this scenario, our script needs to manipulate three screen objects:

  • The textbox where the search string is typed
  • The search button to be clicked in order to submit the search query
  • The text field that displays the number of search results

Our object map will a simple .properties text file that we add to our Selenium project:
Our object mapThe key for each object, for example bing.homepage.textbox, is a logical name for the object that we will use in our script. The corresponding value consists of two parts: the attribute type used for uniquely identifying the object on screen and the corresponding attribute value. For example, the aforementioned text box is uniquely identified by its id attribute, which has the value sb_form_q.

Retrieving objects from the object repository
To retrieve objects from our newly created object map, we will define an ObjectMap with a constructor taking a single argument, which is the path to the .properties file:

public class ObjectMap {
	
	Properties prop;
	
	public ObjectMap (String strPath) {
		
		prop = new Properties();
		
		try {
			FileInputStream fis = new FileInputStream(strPath);
			prop.load(fis);
			fis.close();
		}catch (IOException e) {
			System.out.println(e.getMessage());
		}
	}

The class contains a single method getLocator, which returns a By object that is used by the Selenium browser driver object (such as a HtmlUnitDriver or a FirefoxDriver):

public By getLocator(String strElement) throws Exception {
		
		// retrieve the specified object from the object list
		String locator = prop.getProperty(strElement);
		
		// extract the locator type and value from the object
		String locatorType = locator.split(":")[0];
		String locatorValue = locator.split(":")[1];
		
		// for testing and debugging purposes
		System.out.println("Retrieving object of type '" + locatorType + "' and value '" + locatorValue + "' from the object map");
		
		// return a instance of the By class based on the type of the locator
		// this By can be used by the browser object in the actual test
		if(locatorType.toLowerCase().equals("id"))
			return By.id(locatorValue);
		else if(locatorType.toLowerCase().equals("name"))
			return By.name(locatorValue);
		else if((locatorType.toLowerCase().equals("classname")) || (locatorType.toLowerCase().equals("class")))
			return By.className(locatorValue);
		else if((locatorType.toLowerCase().equals("tagname")) || (locatorType.toLowerCase().equals("tag")))
			return By.className(locatorValue);
		else if((locatorType.toLowerCase().equals("linktext")) || (locatorType.toLowerCase().equals("link")))
			return By.linkText(locatorValue);
		else if(locatorType.toLowerCase().equals("partiallinktext"))
			return By.partialLinkText(locatorValue);
		else if((locatorType.toLowerCase().equals("cssselector")) || (locatorType.toLowerCase().equals("css")))
			return By.cssSelector(locatorValue);
		else if(locatorType.toLowerCase().equals("xpath"))
			return By.xpath(locatorValue);
		else
			throw new Exception("Unknown locator type '" + locatorType + "'");
	}

As you can see, objects can be identified using a number of different properties, including object IDs, CSS selectors and XPath expressions.

Using objects in your test script
Now that we can retrieve objects from our object map, we can use these in our scripts to execute the desired scenario:

public static void main (String args[]) {

		// Create a new instance of the object map
		ObjectMap objMap = new ObjectMap("objectmap.properties");

		// Start a browser driver and navigate to Google
		WebDriver driver = new HtmlUnitDriver();
        driver.get("http://www.bing.com");

        // Execute our test
        try {
        	
        	// Retrieve search text box from object map and type search query
        	WebElement element = driver.findElement(objMap.getLocator("bing.homepage.textbox"));
			element.sendKeys("Alfa Romeo");
			
			// Retrieve search button from object map and click it
			element = driver.findElement(objMap.getLocator("bing.homepage.searchbutton"));
			element.click();
			
			// Retrieve number of search results using results object from object map
			element = driver.findElement(objMap.getLocator("bing.resultspage.results"));
			System.out.println("Search result string: " + element.getText());
			
			// Verify page title
			Assert.assertEquals(driver.getTitle(), "Alfa Romeo - Bing");
			
		} catch (Exception e) {
			System.out.println("Error during test execution:\n" + e.toString());
		}
        
	}

You can see from this code sample that using an object from the object map in your test is as easy as referring to its logical name (i.e., the key in our object map).

Object repository maintenance
With this straightforward mechanism we have been able to vastly reduce the amount of time needed for script maintenance in case object properties change. All it takes is an update of the appropriate entries in the object map and we’re good to go and run our tests again.

Thanks to Selenium Master for explaining this concept clearly for me to apply.

An example Eclipse project using the pattern described above can be downloaded here.

Generating and deploying web service stubs using WSO2

In case you need a relatively simple stub that simulates a third-party web service or application during your testing process, you have several options at your disposal. In this article, I will introduce the WSO2 Enterprise Service Bus, which is one of these options, and show you how you can use it to quickly simulate an existing web service. This is particularly useful when the actual web service or application is not (always) available during testing, or when you want to simulate particular behavior in your testing process.

The WSO2 Enterprise Service Bus
The WSO2 Enterprise Service Bus is a lightweight, open source ESB implementation that has gained significant popularity in recent years. It is used by companies such as Ebay, British Airways and Volvo. Downloading and installation is simple: go to the product website and download a zip file from there. Unpack it and run wso2server.bat (Windows) or wso2server.sh (Linux) from the /bin directory. Note that you need a Java JDK to be installed for WSO2 ESB to run on your system.

Developing WSO2 stubs in Eclipse
As we are going to generate our own web service stub and deploy it using WSO2, it is a good idea to do all the work from within our IDE. I prefer to use Eclipse for this. Stop WSO2 using Ctrl+C as we are going to restart it from within Eclipse again later on. Start Eclipse and install the WSO2 Developer Studio using the Eclipse Marketplace.

stub_install_wso2_developer_studio

Generating a web service skeleton
For this example, I used a sample web service providing weather forecasts for cities in the US. Its WSDL is available here. In order to be able to generate a stub for this web service using WSO2, you should download it to your local machine.

In Eclipse, choose File > New > Project … > WSO2 > Service Hosting > Project Types > Axis2 Service Project. Next, select ‘Create New Axis2 Service Using WSDL File’ (this is why we need a copy of the WSDL on our system). Select the WSDL file and give your project a name:

stub_generate_skeleton_from_wsdl

Click finish to generate a web service skeleton for the service described in the WSDL.

Deploying the web service
To deploy our web service and interact with it, we need to carry out two steps. First, create a Carbon Application Project in Eclipse. This project bundles our web service project so it can be deployed. Select File > New … > Carbon Application Project, give the project a name, select our generated service as a dependency and click Finish.

stub_create_carbon_project

Next, we need to create a server in Eclipse on which to deploy the web service. Add a new server and choose WSO2 > WSO2 Carbon 4.0 based server as the server type. Click Next and select the installation directory of WSO2 as your CARBON_HOME folder. Click Next twice and add the Carbon Application project for our web service to the web server in the ‘Add and Remove’ window (this can be done later as well by right-clicking on the server and selecting ‘Add and Remove’). Finally, start the server.

stub_service_deployed

Testing the web service
Now that our web service skeleton has been deployed, let’s see whether we can communicate with it. The WSDL for the local web service implementation can be found using the WSO2 Management Console (user: admin, pass: admin) that is automatically opened when you start the server in Eclipse. You can then use a tool such as SmartBear SoapUI or Parasoft SOAtest to test the web service skeleton.

By default, none of the operations in the web service will be implemented yet, and sending a request message will result in a response containing a SOAP Fault:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
 <soapenv:Body>
  <soapenv:Fault>
   <faultcode>soapenv:Server</faultcode>
   <faultstring>Please implement com.cdyne.ws.weatherws.WeatherSkeleton#getCityForecastByZIP</faultstring>
   <detail/>
  </soapenv:Fault>
 </soapenv:Body>
</soapenv:Envelope>

The good news is that we have successfully created a web service skeleton from the WSDL web service description, that we deployed it on a local web server for testing purposes and that we are able to communicate with the web service, without having to write a single line of code.

Implementing the web service operations
In order for our web service simulation to return more intelligent responses, we will have to implement the methods that construct the actual responses. When you generate a web service skeleton using the method and example WSDL above, these methods will all be located in the WeatherSkeleton.java file. After generating the skeleton, the only action performed by each method is to throw the exception message we have seen when we first communicated with the web service:

public com.cdyne.ws.weatherws.GetCityForecastByZIPResponse getCityForecastByZIP (com.cdyne.ws.weatherws.GetCityForecastByZIP getCityForecastByZIP) {
            //TODO : fill this with the necessary business logic
            throw new  java.lang.UnsupportedOperationException("Please implement " + this.getClass().getName() + "#getCityForecastByZIP");
}

A simple implementation of this method, generating the same response no matter what is in the request message,is shown below:

public GetCityForecastByZIPResponse getCityForecastByZIP(GetCityForecastByZIP getCityForecastByZIP) {
	 
	 // generate new response for the web service
	 try {
		 GetCityForecastByZIPResponse cityForecastResponse = GetCityForecastByZIPResponse.class.newInstance();
		 
		 // create a new response object and the required objects to fill it
		 ForecastReturn fr = new ForecastReturn();
		 ArrayOfForecast aof = new ArrayOfForecast();
		 Forecast forecast = new Forecast();
		 Calendar cal = Calendar.getInstance();
		 Temp temp = new Temp();
		 POP pop = new POP();
		 
		 // set field values
		 fr.setCity("Boulder");
		 fr.setState("Colorado");
		 fr.setWeatherStationCity("Boulder");
		 fr.setResponseText("Forecast for Boulder, generated on " + new SimpleDateFormat("MM/dd/yyyy HH:mm:ss").format(cal.getTime()));
		 fr.setSuccess(true);
		 
		 // create weather forecast array and forecast entry
		 forecast.setDate(cal);
		 forecast.setDesciption("Sample forecast");
		 temp.setDaytimeHigh("90");
		 temp.setMorningLow("40");
		 forecast.setTemperatures(temp);
		 pop.setDaytime("12:00:00");
		 pop.setNighttime("00:00:00");
		 forecast.setProbabilityOfPrecipiation(pop);
		 
		 // add entry to forecast array
		 aof.addForecast(forecast);
		 
		 // add forecast to response and return it
		 fr.setForecastResult(aof);
		 cityForecastResponse.setGetCityForecastByZIPResult(fr);
		 return cityForecastResponse;
		 
	 } catch (Exception e) {
		 throw new UnsupportedOperationException("Unexpected error occurred during message processing");
	 }
 }

When we redeploy our web service (by right-clicking on the server in Eclipse and selecting ‘Redeploy’) and call the GetCityForecastByZIP operation again, we now get a sensible response from our web service:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
   xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
 <soapenv:Body>
  <ns1:GetCityForecastByZIPResponse xmlns:ns1="http://ws.cdyne.com/WeatherWS/">
   <ns1:GetCityForecastByZIPResult>
    <ns1:Success>true</ns1:Success>
    <ns1:ResponseText>Forecast for Boulder, generated on 11/07/2013 09:02:54</ns1:ResponseText>
    <ns1:State>Colorado</ns1:State>
    <ns1:City>Boulder</ns1:City>
    <ns1:WeatherStationCity>Boulder</ns1:WeatherStationCity>
    <ns1:ForecastResult>
     <ns1:Forecast>
      <ns1:Date>2013-11-07T09:02:54.274+01:00</ns1:Date>
      <ns1:WeatherID>0</ns1:WeatherID>
      <ns1:Desciption>Sample forecast</ns1:Desciption>
      <ns1:Temperatures>
       <ns1:MorningLow>40</ns1:MorningLow>
       <ns1:DaytimeHigh>90</ns1:DaytimeHigh>
      </ns1:Temperatures>
      <ns1:ProbabilityOfPrecipiation>
       <ns1:Nighttime>00:00:00</ns1:Nighttime>
       <ns1:Daytime>12:00:00</ns1:Daytime>
      </ns1:ProbabilityOfPrecipiation>
     </ns1:Forecast>
    </ns1:ForecastResult>
   </ns1:GetCityForecastByZIPResult>
  </ns1:GetCityForecastByZIPResponse>
 </soapenv:Body>
</soapenv:Envelope>

In future posts, I will introduce ways to improve the functionality and flexibility of our simulated web service, and make it even more useful when used in our testing processes.

A very basic web service test tool

For those of us involved in testing web services and SOA-based applications, there are lots of different test tools on the market that can help speed up the testing and make them repeatable and easy to maintain. Some examples of these tools are SoapUI from SmartBear and SOAtest from Parasoft. SoapUI is available both as a freeware product and as an enterprise edition offering additional functionality. SOAtest is only available in a paid version.

One of the biggest downsides of the freeware version of SoapUI is the lack of data driven test support. This feature is only available in the paid version. In a previous post I introduced a simple way of implementing data driven testing in Selenium Webdriver tests, using Microsoft Excel sheets as the test data container format. As I am very regularly involved in web service testing myself, and often want to use data driven testing to create maintainable and flexible test suites, I thought it would be a good idea to see whether it’s possible to quickly create a very basic web service testing tool that supports data driven testing.

The test data source
As in the previous example, let’s create a test data source first. I chose to base the solution presented here on predefined XML request message files as this saves us the trouble of creating XML objects in our code. There are obvious downsides to this, of course, but for now we will focus on the ability to call a web service and subsequently capture and validate the result.

Our test data source looks like this:

webservice_test_data_source

It contains columns to identify the current test case, the path to the XML file containing the SOAP message to be sent and the endpoint to which the message should be sent. The last two columns contain the name of an element in the web service response we would like to validate and its expected value, respectively.

Reading our test data source
In our very basic web service test tool, we will process this Excel sheet just as we did in the Selenium example posted earlier. For every test data row, we will then execute the following steps:

  • Create a SOAP request from the XML file
  • Send the SOAP request to the right web service endpoint and capture the response
  • Extract the value from a response message element and compare it to the expected value

Create a SOAP request from the XML file
This is an easy step, as all we need to do is open the file, read all of its contents and transform it into a SOAP message object:

private static SOAPMessage createSOAPRequest(String strPath) throws Exception {
        
    // Create a SOAP message from the XML file located at the given path
    FileInputStream fis = new FileInputStream(new File(strPath));
    MessageFactory factory = MessageFactory.newInstance();
    SOAPMessage message = factory.createMessage(new MimeHeaders(), fis);
    return message;
}

Send the SOAP request to the right web service and capture the response
This is fairly straightforward as well and can be done using standard Java methods:

private static SOAPMessage getSOAPResponse(SOAPMessage soapRequest, String strEndpoint) throws Exception, SOAPException {
    	
    // Send the SOAP request to the given endpoint and return the corresponding response
    SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance();
    SOAPConnection soapConnection = soapConnectionFactory.createConnection();
    SOAPMessage soapResponse = soapConnection.call(soapRequest, strEndpoint);
    return soapResponse;	
}

Validating elements from the response message
Finally, we will parse the response message and validate the value of one of its elements. The validation result is sent to the stdout:

private static void validateValue(SOAPMessage soapMsg, String strEl, String strExpected) throws Exception {
    	
    // Get all elements with the requested element tag from the SOAP message
    SOAPBody soapBody = soapMsg.getSOAPBody();
    NodeList elements = soapBody.getElementsByTagName(strEl);
        
    // Check whether there is exactly one element with the given tag
    if (elements.getLength() != 1){
        System.out.println("Expected exactly one element " + strEl + "in message, but found " + Integer.toString(elements.getLength()));
    } else {
        // Validate the element value against the expected value
        String strActual = elements.item(0).getTextContent();
        if (strActual.equals(strExpected)) {
        	System.out.println("Actual value " + strActual + " for element " + strEl + " matches expected value");
        } else {
        	System.out.println("Expected value " + strExpected + " for element " + strEl + ", but found " + strActual + " instead");
        }
    }
}

Running the test
When we run the test, we see that the web service to be tested is called three times. Two tests succeed, the last test case fails (on purpose, to show that the validation is executed properly):

webservice_console_output

Extensions to our tool
As stated earlier in this post, there are a lot of improvements to be made to this very basic web service test tool. For instance, we could add:

  • Dynamic request message generation based on a template and data source values
  • XSD validation for the response messages
  • Support for non-SOAP-based (or plain XML) web services or even for transport protocols other than HTTP

Some of these improvements will probably be featured in later articles at ontestautomation.com. For questions or suggestions on topics to be covered on this blog, please do not hesitate to contact me at bas AT ontestautomation.com or leave a reply through the comment form below.

An example Eclipse project using the pattern described above can be downloaded here.