Salesforce Ant Scripts – Selenium

The Salesforce metadata API is an extremely powerful tool, when combined with Ant, Jenkins etc. for build automation. There is however a number of configuration items that simply can’t be retrieved and deployed using this API (Account Teams, Support Settings, Lead Settings, Case Assignment and Escalation Rules etc.). The unsupported list can be found here, unfortunately the platform expands at a rate more or less equal to the rate at which coverage of the API has increased over time. Anyway, my point here is that typically deployments have three steps; a manual step to cover the gaps in the metadata API (pre-requisites), an automated deployment step (retrieve-and-deploy with Ant) and finally a data population step (Data Loader CLI with Ant perhaps..). Leaving data to one side (for this post), an ability to merge steps 1 and 2 would enable full automation of the deployment of configuration – which in most cases would be a good thing. One approach to automate step 1 is to write Selenium web browser automation scripts which drive the Salesforce application at the UI level. The scripts can be exported as JUnit test cases and then be incorporated into an Ant based build process and automated. My approach to doing this is outlined below, as with most things there are many ways to achieve the same result and I’m sure this can be improved on, however it keeps the process simple and gets the job done which tends to work for me. Additionally, the approach plays well with Ant, Jenkins/Hudson etc.. so it should be straightforward to extend an existing build process.

1. Install the Selenium IDE Firefox Extension.
2. Using Selenium IDE record the act of logging-in to Salesforce and making the required configuration changes.
3. Export the test case as a Java / JUnit 4 / WebDriver file. This creates a .java file as below. The example simply creates a Chatter post for the logged-in user, hopefully this is simple and illustrative enough to make the point.

package com.example.tests;

import java.util.regex.Pattern;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.Select;

public class SeleniumTest {
  private WebDriver driver;
  private String baseUrl;
  private boolean acceptNextAlert = true;
  private StringBuffer verificationErrors = new StringBuffer();

  @Before
  public void setUp() throws Exception {
    driver = new FirefoxDriver();
    baseUrl = "https://test.salesforce.com/";
    driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
  }

  @Test
  public void testSelenium() throws Exception {
    driver.get(baseUrl + "/");
    driver.findElement(By.id("username")).clear();
    driver.findElement(By.id("username")).sendKeys("release.manager@force365.com");
    driver.findElement(By.id("password")).clear();
    driver.findElement(By.id("password")).sendKeys("mypassword");
    driver.findElement(By.id("Login")).click();
    driver.findElement(By.id("publishereditablearea")).clear();
    driver.findElement(By.id("publishereditablearea")).sendKeys("new Chatter post - Selenium");
    driver.findElement(By.id("publishersharebutton")).click();
  }

  @After
  public void tearDown() throws Exception {
    driver.quit();
    String verificationErrorString = verificationErrors.toString();
    if (!"".equals(verificationErrorString)) {
      fail(verificationErrorString);
    }
  }

  private boolean isElementPresent(By by) {
    try {
      driver.findElement(by);
      return true;
    } catch (NoSuchElementException e) {
      return false;
    }
  }

  private String closeAlertAndGetItsText() {
    try {
      Alert alert = driver.switchTo().alert();
      if (acceptNextAlert) {
        alert.accept();
      } else {
        alert.dismiss();
      }
      return alert.getText();
    } finally {
      acceptNextAlert = true;
    }
  }
}

4. Modify the test case java code as required.
5. Download the Java Selenium Client Driver from http://seleniumhq.org/download/
6. Extend or create a new Ant build file to compile and execute the test case. My example below requires a [selenium\src] sub directory structure in the build root, with the .java test case files placed in the src directory.

<project basedir="." default="usage" name="invoke Selenium script to configure Salesforce org">
	<property name="bin" value=".\selenium\bin" />
	<property name="lib" value="c:\Release Management\selenium-2.28.0\libs" />
	<property name="src" value=".\selenium\src" />
	<property name="report" value=".\selenium\reports" />	
	
 	<target name="usage" depends="">
		<echo message="Compiles and executes Selenium IDE exported test cases (source format JUnit4 WebDriver .java files)" />
	</target>

	<target name="init">
		<delete dir="${bin}" />
		<mkdir dir="${bin}" />
	</target>

	<target name="compile" depends="init">
		<javac includeantruntime="false" source="1.7" srcdir="${src}" fork="true" destdir="${bin}" >
			<!-- requires Selenium test cases exported as JUnit4 WebDriver .java files in the src sub-directory -->
			<classpath>
		    	<pathelement path="${bin}">
		        </pathelement>
		        <fileset dir="${lib}">
		        	<include name="**/*.jar" />
		        </fileset>
		  	</classpath>
		</javac>
	</target>

	<target name="exec" depends="compile">
		<delete dir="${report}" />
		<mkdir dir="${report}" />
		<mkdir dir="${report}/xml" />
		
		<junit printsummary="yes" haltonfailure="yes">
			<classpath>
		    	<pathelement path="${bin}">
		    	</pathelement>
		        <fileset dir="${lib}">
		        	<include name="**/*.jar" />
		        </fileset>
		  	</classpath>
			<test name="com.example.tests.SeleniumTest" haltonfailure="yes" todir="${report}/xml" outfile="SeleniumTest-result">
				<formatter type="xml" />
			</test>
		</junit>
		
		<junitreport todir="${report}">
	        <fileset dir="${report}/xml">
	            <include name="TEST*.xml" />
	        </fileset>
	        <report format="frames" todir="${report}/html" />
	    </junitreport>		
	</target>
</project>

Note. There is no need to start or stop a Selenium server as the script runs locally on the build server – Firefox will be required however if you stick with the default browser in recorded scripts.

I’ll follow this initial post with further detail on the following;
1. Conditional script logic – i.e. I want the script to check for a condition before making a change such that it selectively configures and therefore won’t be reliant on a clean, predictable state.
2. Execution of test suites rather than individual cases.
3. Most likely I’ll refine the build.xml example as I understand more about this.