Tuesday, October 15, 2013

Cucumber and Maven

Cucumber and Maven

Goal

To run Cucumber specifications via Maven.

tl;dr

A maven plug-in for running Cucumber features via Cucumber-JVM is available on github and will be a helpful resource for following this article. There is also a maven archetype for generating a Cucumber-JVM project. Acceptance tests for the dropbox-maven-plugin and jacoco-scala-maven-plugin both run using this plugin and can be used as templates for your own specifications. The interesting technologies showcased include Cucumber-JVM and JRuby.

The Project

Suppose our product owner would like to specify the behavior of our product in a way that can be verified automatically and continuously, for example, using Gherkin. Now suppose we would like to configure a Maven build system to run these acceptance tests.

Using Ant Runner

The maven-antrun-plugin can often be the fallback when there is no maven plug-in for a specific purpose. Indeed there are instructions and examples of using the maven-antrun-plugin to spawn the command-line versions of both JRuby and Cucumber-JVM for initializing a Cucumber project, installing its required bundles and executing its specifications.
A project with a Gemfile in its root, features in its src/test/features directory and Ruby step definitions in its src/test/features/step_definitions directory can use a pom configuration like this to install and run its acceptance tests.
The project layout

  my-cucumber-jvm-specifications
  .
  |____Gemfile
  |____pom.xml
  | src
  | | test
  | | | features
  | | |____my.feature
  | | | | step_definitions
  | | | |____my_steps.rb


pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>timezra.maven</groupId>
  <artifactId>cucumber-jvm-from-ant</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <name>cucumber-jvm-from-ant</name>

  <properties>
    <cucumber-jvm-version>1.1.5</cucumber-jvm-version>
    <features-directory>src/test/features</features-directory>
    <gems-directory>${project.build.directory}/gems</gems-directory>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.jruby</groupId>
      <artifactId>jruby-complete</artifactId>
      <version>1.7.4</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>info.cukes</groupId>
      <artifactId>cucumber-jruby</artifactId>
      <version>${cucumber-jvm-version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <testResources>
      <testResource>
        <directory>${basedir}/${features-directory}</directory>
        <filtering>true</filtering>
      </testResource>
    </testResources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-antrun-plugin</artifactId>
        <version>1.7</version>
        <executions>
          <execution>
            <phase>generate-test-resources</phase>
            <configuration>
              <target>
                <sequential>
                  <echo message="Installing bundler gem" />
                  <java jar="${maven.dependency.org.jruby.jruby-complete.jar.path}"
                    fork="true" failonerror="true" maxmemory="256m"
                    newenvironment="true">
                    <arg value="-S" />
                    <arg value="gem" />
                    <arg value="install" />
                    <arg value="bundler" />
                    <arg value="-i" />
                    <arg value="${gems-directory}" />
                    <arg value="--no-ri" />
                    <arg value="--no-rdoc" />
                  </java>
                  <echo message="Doing bundle install" />
                  <java jar="${maven.dependency.org.jruby.jruby-complete.jar.path}"
                    fork="true" failonerror="true" maxmemory="512m"
                    newenvironment="true">
                    <env key="GEM_HOME" path="${gems-directory}" />
                    <arg value="-S" />
                    <arg value="${gems-directory}/bin/bundle" />
                    <arg value="install" />
                    <arg value="--gemfile=${basedir}/Gemfile" />
                  </java>
                </sequential>
              </target>
            </configuration>
            <goals>
              <goal>run</goal>
            </goals>
          </execution>
          <execution>
            <phase>test</phase>
            <id>run-cucumbers</id>
            <goals>
              <goal>run</goal>
            </goals>
            <configuration>
              <target>
                <echo message="Running Cucumber Ruby Features" />
                <java fork="true" classname="cucumber.api.cli.Main"
                  classpathref="maven.test.classpath" failonerror="true">
                  <env key="GEM_HOME" path="${gems-directory}" />
                  <env key="RUBY_VERSION" value="2.0" />
                  <arg value="-f" />
                  <arg value="pretty" />
                  <arg value="--glue" />
                  <arg value="${features-directory}" />
                  <arg value="${features-directory}" />
                </java>
              </target>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

Using the JRuby and Cucumber-JVM Plug-ins

This pom forks new processes for installing bundler, installing the bundles required by the tests and running the tests themselves. The jruby-maven-plugin and cucumber-jvm-maven-plugin can be configured to run specifications without relying on the maven-antrun-plugin to execute new processes. Configuration is similar to the command-line JRuby and Cucumber-JVM clients, and in fact, each plug-in can take a raw CLI configuration if the built-in goals are not sufficient.
Given the same project layout as above, this pom will perform the identical steps in the same Maven process.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>timezra.maven</groupId>
  <artifactId>cucumber-jvm-from-maven</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <name>cucumber-jvm-from-maven</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <gems-directory>${project.build.directory}/gems</gems-directory>
    <jruby-version>1.7.4</jruby-version>
    <cucumber-jvm-version>1.1.5</cucumber-jvm-version>
  </properties>

  <profiles>
    <profile>
      <id>default-profile</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <properties>
        <runCucumbers>true</runCucumbers>
      </properties>
    </profile>
    <profile>
      <id>skip-tests</id>
      <activation>
        <activeByDefault>false</activeByDefault>
        <property>
          <name>skipTests</name>
          <value>true</value>
        </property>
      </activation>
      <properties>
        <runCucumbers>false</runCucumbers>
      </properties>
    </profile>
    <profile>
      <id>run-tests</id>
      <activation>
        <activeByDefault>true</activeByDefault>
        <property>
          <name>runCucumbers</name>
          <value>true</value>
        </property>
      </activation>
      <build>
        <testResources>
          <testResource>
            <directory>src/test/features</directory>
            <filtering>true</filtering>
          </testResource>
        </testResources>
        <plugins>
          <plugin>
            <groupId>timezra.maven</groupId>
            <artifactId>jruby-maven-plugin</artifactId>
            <version>${jruby-version}</version>
            <executions>
              <execution>
                <id>install-bundles</id>
                <phase>pre-integration-test</phase>
                <goals>
                  <goal>gem-install</goal>
                  <goal>bundle-install</goal>
                </goals>
                <configuration>
                  <gem_home>${gems-directory}</gem_home>
                  <gem>bundler</gem>
                  <gemfile>${project.basedir}/Gemfile</gemfile>
                </configuration>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>timezra.maven</groupId>
            <artifactId>cucumber-jvm-maven-plugin</artifactId>
            <version>${cucumber-jvm-version}</version>
            <<dependencies>
              <dependency>
                <groupId>org.jruby</groupId>
                <artifactId>jruby-complete</artifactId>
                <version>${jruby-version}</version>
              </dependency>
            </dependencies>
            <executions>
              <execution>
                <id>run-cucumbers</id>
                <phase>integration-test</phase>
                <goals>
                  <goal>jruby</goal>
                </goals>
                <configuration>
                  <gem_home>${gems-directory}</gem_home>
                  <feature>${project.build.testOutputDirectory}</feature>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
  <pluginRepositories>
    <pluginRepository>
      <id>tims-repo</id>
      <url>http://timezra.github.com/maven/releases</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </pluginRepository>
  </pluginRepositories>
</project>
NB: If you have set the $GEM_PATH, $GEM_HOME or $RUBY_VERSION environment variables or if you are using RVM (which sets these for you), you will need to unset them before running your specs. When forking a new Java process to run JRuby or Cucumber-JVM, it is possible to configure these new environment variables in the pom, but when running these goals in-process in Maven, these variables must be unset before the run.

Using the Cucumber-JVM Archetype

There is a Maven archetype for this type of acceptance testing project that will help you get started. With a single command, the boilerplate project layout and pom configuration will be generated automatically so that you can start writing specifications and step definitions right away.

  $ mvn archetype:generate -DarchetypeCatalog=http://timezra.github.com/maven/releases/archetypes -Dfilter=timezra.maven:cucumber-jvm-archetype
    fill in the groupId, artifactId, version and feature information
    cd to the new project
  $ unset GEM_PATH GEM_HOME RUBY_VERSION
  $ mvn verify


Conclusion

This post describes the layout and configuration of a Cucumber-JVM project using both forked java processes and in-process Maven plug-ins to run acceptance tests. A Maven archetype can generate all the boilerplate project configuration, so with a few simple commands you can begin to write Gherkin specifications and run them as part of your continuous build process.

No comments: