Thursday, October 18, 2012

Running Selenium Testing in Parallel with JUnit

Recently I have been asked to speed up our Selenium regression test suite (currently at 2 hours) by running tests in parallel. We are using Jenkins to kick off the ant task that will run the test suite that is configured in Junit.

There are 2 parts to getting this to work:
  •         The Selenium Grid2 Hub & Node
  •         Junit's parallel thread process

As usual my first task is to get a prototype running locally. I started by reading the Selenium Grid2 documentation which is pretty solid.

I downloaded the selenium-server-standalone-2.25.0.jar and set up a couple of bat scripts to start the grid.
  •     startHub.bat
    java -jar selenium-server-standalone-2.25.0.jar -role hub
  •     startNode.bat
    java -jar selenium-server-standalone-2.25.0.jar -role node  -hub http://localhost:4444/grid/register -browser browserName=firefox,maxInstances=5 -browser "browserName=internet explorer,maxInstances=5" -maxSession 5

Place these in the dir with the jar, run the hub, run the node and you should be ready to rock. The console can be viewed at: http://localhost:4444/grid/console


Lets look at the code required to send the tests to the grid using the remote web driver. For completeness I've also included the code for setting up a proxy server and a regular web driver if the hub is not running.

 /**
     * Returns an instance of the configured remote web driver pointing to a local web hub on port 4444
     *
     * This is for use in stage. If the hub is not running a regular web driver is returned
     *
     * @return WebDriver hub
     */
    private WebDriver getWebDriver() throws UnknownHostException {
        try {
            return new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), getDesiredCapabilities());
        //if the remote selenium hub is unavailable return a regular web driver
        } catch (MalformedURLException e) {
            return getLocalWebDriver();
        } catch (UnreachableBrowserException e) {
            return getLocalWebDriver();
        }
    }
  
    /**
     * Returns an instance of the configured web driver.
     *
     * This is for local testing
     *
     * @return WebDriver
     */
    private WebDriver getLocalWebDriver() throws UnknownHostException {
        if (useFirefox()) {
            return new FirefoxDriver(getDesiredCapabilities());
        } else {
            return new InternetExplorerDriver(getDesiredCapabilities());
        }
    }
  
    /**
     * Returns a DesiredCapabilities for either IE or Firefox.
     *
     * @return DesiredCapabilities for either IE or Firefox
     * @throws UnknownHostException
     */
    private DesiredCapabilities getDesiredCapabilities() throws UnknownHostException {
        if (useFirefox()) { //firefox
            DesiredCapabilities dc = DesiredCapabilities.firefox();
            FirefoxProfile profile = new FirefoxProfile();
            if (useProxy())
                dc.setCapability(CapabilityType.PROXY, getSeleniumProxy());
            if (getLocale() != null)
                profile.setPreference("intl.accept_languages", getLocale().toString());
            dc.setCapability(FirefoxDriver.PROFILE, profile);
            return dc;
        } else { //internet explorer
            DesiredCapabilities dc = DesiredCapabilities.internetExplorer();
            if (useProxy())
                dc.setCapability(CapabilityType.PROXY, getSeleniumProxy());
            return dc;
        }
    }
  
    /**
     * This will return a Selenium configured proxy pointing to a local proxy
     * on port 6666
     *
     * @param server
     * @return
     * @throws UnknownHostException
     */
    private Proxy getSeleniumProxy() throws UnknownHostException {
        Proxy proxy = new Proxy();
        proxy.setProxyType(Proxy.ProxyType.MANUAL);
        String proxyStr = String.format("%s:%d", InetAddress.getLocalHost().getCanonicalHostName(), proxyPort);
        proxy.setHttpProxy(proxyStr);
        proxy.setSslProxy(proxyStr);
        proxy.setNoProxy("someaddress");
        return proxy;
    }

The next step is to set up our build.xml ant task to kick of the tests in parallel.

Thanks to code cop I've come up with this partial build.xml:
    <target name="run-tests-dev-parallel4" depends="compile">
      <parallel threadcount="4">
        <antcall target="-junitThread">
          <param name="junit.division.total" value="4" />
          <param name="junit.division.num" value="1" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="4" />
          <param name="junit.division.num" value="2" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="4" />
          <param name="junit.division.num" value="3" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="4" />
          <param name="junit.division.num" value="4" />
        </antcall>
      </parallel>
    </target>
   
    <target name="run-tests-dev-parallel8" depends="compile">
      <parallel threadcount="8">
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="1" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="2" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="3" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="4" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="5" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="6" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="7" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="8" />
        </antcall>
      </parallel>
    </target>
   
    <target name="-junitThread">
        <echo message="started thread ${junit.division.num} of ${junit.division.total}" />
        <junit fork="true" forkmode="perBatch" printsummary="on" haltonfailure="false" failureproperty="tests.failed" showoutput="true">
        <classpath refid="dev-classpath" />
        <formatter type="xml" />        
        <batchtest todir="${dev.test.results.dir}">
            <fileset dir="${dev.bin.dir}">
                <include name="**/*Test.class" />
                <custom classname="fully.qualified.DividingSelector" classpath="bin">
                  <param name="divisor" value="${junit.division.total}" />
                  <param name="part" value="${junit.division.num}" />
                </custom>
            </fileset>
        </batchtest>
      </junit>
      <echo message="ended thread ${junit.division.num} of ${junit.division.total}" />
      <fail if="tests.failed">
                        tests.failed=${tests.failed}
                        ***********************************************************
                        ***********************************************************
                        ****  One or more tests failed!  Check the output ...  ****
                        ***********************************************************
                        ***********************************************************
       </fail>
    </target>

The DividingSelector.java is as follows:
import java.io.File;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.Parameter;
import org.apache.tools.ant.types.selectors.BaseExtendSelector;

/**
 * Split a fileset into the given number of parts.
 *
 */
public final class DividingSelector extends BaseExtendSelector {
   
    /**
     * The num of the current test class we are processing, 1 to totalTests
     */
    private int testNum;

    /**
     * Number of total threads to divide the tests between, 1 to totalThreads
     */
    private int totalThreads;

    /**
     * Current thread to run a test
     */
    private int curThread;

    public void setParameters(Parameter[] pParameters) {
        super.setParameters(pParameters);
        for (int j = 0; j < pParameters.length; j++) {
            Parameter p = pParameters[j];
            if ("divisor".equalsIgnoreCase(p.getName())) {
                totalThreads = Integer.parseInt(p.getValue());
            } else if ("part".equalsIgnoreCase(p.getName())) {
                curThread = Integer.parseInt(p.getValue());
            } else {
                throw new BuildException("unsupported parameter " + p.getName() + " with value " + p.getValue());
            }
        }
    }

    public void verifySettings() {
        super.verifySettings();
        if (totalThreads <= 0 || curThread <= 0) {
            throw new BuildException("parameter part or divisor not set");
        }
        if (curThread > totalThreads) {
            throw new BuildException("parameter part must be <= divisor");
        }
    }

    public boolean isSelected(File pBaseDir, String pRelative, File pFullPath) {
        //increment the test counter
        testNum++;
       
        //this is the thread that will run the current test
        int threadNum = testNum % totalThreads + 1;
        boolean result = threadNum == curThread;
           
        if (result)
            System.out.println("thread: " + curThread + " ,running: " + pRelative + " , testnum: " + testNum);

        return result;
    }

}

Once you have the targets working you can configure it in Jenkins to run the run-tests-dev-parallel8 or run-tests-dev-parallel4 target depending on what works better for your build server.


When you run the build you'll see in the console output junit spawning the 8 threads and sending them to the hub, the hub will then handle the distribution to the nodes running the actual tests.

run-tests-dev-parallel8:

-junitThread:
     [echo] started thread 6 of 8

-junitThread:
     [echo] started thread 7 of 8

-junitThread:
     [echo] started thread 5 of 8

-junitThread:
     [echo] started thread 1 of 8

-junitThread:
     [echo] started thread 8 of 8

-junitThread:
     [echo] started thread 2 of 8

-junitThread:
     [echo] started thread 3 of 8

-junitThread:
     [echo] started thread 4 of 8

It will then pre divide the tests between the threads and will printout which thread has been allocated to which test. This can cause problems of a long running thread which is left after everything else is finished. This can be build into the DividingSelector but in not a prefect solution.

Next we will look at running the hub and node as a windows service on the build server.

References:

http://blog.code-cop.org/2009/09/parallel-junit.html



2 comments:

  1. Hello, I am using this way to get paralellism in the Junit test case execution.

    But I find sometimes problems in this thread execution. In my case, I use 4 threads to execute my tests, but occurs that sometimes the tests fails because just in the moment I want to write some value in some input file, appears some extrange characters. I put an example: in some executions I have programmed that i will write the number 123456789 in one input file, but the final writing is 123´456´789, making the test fails.

    I'm sure that this is because of the paralellism, because executing my test in sequence, this problem doesn't appear.

    Thanks in advance.

    ReplyDelete
  2. Thanks for writing this.Really helpful.Implemented this in my project and it worked!! thanks once again!

    ReplyDelete

Note: Only a member of this blog may post a comment.