Nelz's Blog

Mah blogginess

Maven, TestNG, and SeleniumRC

Today, I set up a Maven module to run SeleniumRC (Java) tests via TestNG. It wasn’t a straightforward or easy task, so I thought I’d share some of what I found with you.

We opt to have all of our modules stay in lock-step version-wise, so we keep a pretty large structure in our SVN trunk. We include/exclude modules for compilation by using Maven profiles, so we don’t have to build the whole structure if we are only changing one small component. Our top-level pom.xml file looks similar to this:

<project>
  <groupId>...
  <artifactId>...
  <version>...
  <packaging>pom</packaging>
  <modules>
    <module>baseModule</module>
  </modules>
  <profiles>
    <profile>
      <id>all</id>
      <modules>
        <module>otherModule</module>
        ...
        <module>web-test</module>
      </modules>
    </profile>
    <profile>
      <id>web-test</id>
      <modules>
        <module>web-test</module>
      </modules>
    </profile>
  </profiles>
</project>

As you can see, running “mvn clean install” at this top level would only end up building the baseModule. To get all the modules, we run “mvn clean install -Pall” which builds baseModule along with otherModule and web-test and anything else as represented by the ellipse. The web-test module is the one we will be interested in.

For our heavy-weight tests (non-unit tests, like functional or web testing), we like to have the module compile, but not run unless we specifically trigger the tests. To achieve this, we create a pom.xml (for the web-test module) that looks something like this:

<project>
  <parent>...
  <artifactId>web-test</artifactId>
  <packaging>pom</packaging>
  <build>
    <testSourceDirectory>src/it/java</testSourceDirectory>
    <testResources>
      <testResource>
        <directory>src/it/resources</directory>
      </testResource>
    </testResources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.4.2</version>
        <configuration>
          <testSourceDirectory>src/it/java</testSourceDirectory>
          <excludedGroups>web-test</excludedGroups>
        </configuration>
        <executions>
          <execution>
            <phase>integration-test</phase>
            <goals>
              <goal>test</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

This pom has several features:

  • It sets up src/it/java as the location holding the source code for the tests
  • It sets the surefire plugin (which runs your tests) to only run at the integration-test part of the Maven lifecycle
  • It says to ignore any tests in the TestNG “web-test” group
  • Note the packaging says this module is of the type “pom”. I found that if this is set to “jar”, the tests run twice for some unknown reason

Now, if you had tests in src/it/java (that were all annotated as “web-test”) and you ran “mvn clean install”, you’d see that zero tests ran. This is what we want! Now, we have to get the tests running if we pass a specific profile in. To achieve this, I added the following profile definition to the web-test pom.xml.

<profiles>
    <profile>
      <id>run-web-test</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.4.2</version>
            <configuration>
              <suiteXmlFiles>
                <suiteXmlFile>src/it/resources/testng.xml</suiteXmlFile>
              </suiteXmlFiles>
              <!--<groups>web-test</groups>-->
              <!--<forkMode>always</forkMode>-->
            </configuration>
            <executions>
              <execution>
                <phase>integration-test</phase>
                <goals>
                  <goal>test</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

This profile is invoked when you run “mvn clean install -Prun-web-test”. There are several things I’d like to point out here:

  • At runtime, this overrides the previously-declared surefire plugin configuration, but only when invoking the profile.
  • Originally, I was mostly getting stuff to work without the <suiteXmlFiles /> section, but with the <groups /> and <forkMode /> sections… But it was running every single test class as it’s own TestSuite.
  • To fix the multiple TestSuite (and @Before*, @After* problems that I’ll discuss later), I ended creating my own testng.xml file. Having this file and using the <suiteXmlFiles /> element made things a whole lot easier for configuring my TestNG executions than trying to configure the tests through the clumsy interference of the surefire plugin.
  • As much as I like Maven, I have to say that the lack of documentation and general obfuscation that happens when using their plugins can really drive a person BATTY!

Now, I need to start integrating the stuff I need for using SeleniumRC. I found some decent help on how to start up and shut down the selenium-server, however I disagree with using a post-integration-test phase to shut down the server, because if the tests fail in the integration-test, Maven won’t progress to the next phase and you’ll be left with a spuriously running selenium-server. The client API’s do have access to the command that the was being run to shut the server down, so it is just a matter of having some TestNG @AfterSuite or @AfterGroup method issue the Selenium.shutDownSeleniumServer() command.

This brings me to some of the TestNG wonkiness I saw. I decided to use a Singleton Data Provider (named  SeleniumFactory) to deliver the Selenium objects to the individual tests. (Selenium objects are time-expensive to create, so I wanted to only create 1 for the duration of the entire test suite.) I wanted to initialize the SeleniumFactory object in a @BeforeGroup method, deliver the Selenium object as a Data Provider, and then tear the SeleniumFactory down in a @AfterGroup method. (At first, I was fighting Maven’s handling of each test as its own test suite which was giving me sequence issues, which is why I ended up defining my own testng.xml file, as I stated above.) Let me start with some code to show you what I mean:

public class SequenceEndsTest {
    @BeforeGroups(groups = {"web-test"})
    public void startupSelenium() {
        System.out.println("-- > Called startup.");
    }
    @AfterGroups(groups = {"web-test"}, alwaysRun = true)
    public void cleanupSelenium() {
        System.out.println("-- > Called cleanup.");
    }
}
public class SeleniumFactory {
    @DataProvider(name = "selenium")
    public static Object[][] getSelenium() {
        System.out.println("-- > Called data provider.");
        return new Object[][] { new Object[] {null}};
    }
}
public class SequenceTest {
    @Test(groups={"web-test"},
            dataProvider = "selenium", dataProviderClass = SeleniumFactory.class)
    public void testSomething(final Selenium selenium) {
        System.out.println("-- > Called test.");
    }
}

This is what I expected to see after running this code:

-- > Called startup.
-- > Called data provider.
-- > Called test.
-- > Called cleanup.

But this is what I actually saw:

-- > Called data provider.
-- > Called startup.
-- > Called test.
-- > Called cleanup.

I would have assumed that an @BeforeGroup method got called before a data provider method, but I guess my assumption is wrong, eh? After I figured out this order-of-method-calls business, I was able to adapt my SeleniumFactory accordingly.

… And that is my story. To summarize: each of these tools could be better documented, but at the end of the day I was able to get a Maven module up and running SeleniumRC tests via TestNG. Now, on to the test-writing!