Friday, August 09, 2013

Integration testing with Spring framework and Cucumber-jvm in maven web application


I was faced with a challenged to do software development without a QA engineer in my team.  In the company that I work with, developers do the unit testing, and QA engineers do the integration testing and end-to-end testing. Unfortunately, our QA engineer has moved on to a different company.

I started looking for a solution to implement some sort of an automatic integration testing for our projects. I came across some integration testing practice with maven while searching  the web. I asked some of my friends about their practices, and one my friends told me that they've been using cucumber (cukes.info). He told me that with cucumber, product managers can write the features (since it is just in plain english) and developers can build that feature. As it turned out, cucumber is more than just an integration-testing framework. it is a behavioral driven development framework.

So I decided to give it a try. I downloaded the cucumber-jvm project, read their examples and play with it. When I encountered some roadblocks, I go to stackoverflow to find the answers and if still I cannot find what I am looking for, I would ask my friends for some advice to see if they have encountered this problem.

At the end, I was able to make it work. Here are the steps that I did for my maven project.

First step is to add the dependencies in your pom.xml

        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>${cucumber.java.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>${cucumber.java.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
              <groupId>info.cukes</groupId>
              <artifactId>cucumber-spring</artifactId>
              <version>${cucumber.java.version}</version>
              <scope>test</scope>
          </dependency>

I am using version 1.1.3. That's the latest version at this time. You can always check the maven repository for newer version.

The next step would be to make sure that you have the tomcat plugin in your project. This will allow your project to run tomcat before your integration test, and stop tomcat after your integration step.

           <plugin>
              <groupId>org.apache.tomcat.maven</groupId>
              <artifactId>tomcat7-maven-plugin</artifactId>
              <version>2.0</version>
              <configuration>
                <path>/keychest-extapi</path>
             </configuration>
              <executions>
                <execution>
                  <id>start-tomcat</id>
                  <phase>pre-integration-test</phase>
                  <goals>
                    <goal>run</goal>
                  </goals>
                  <configuration>
                    <fork>true</fork>
                    <port>9999</port>
                  </configuration>
                </execution>
                <execution>
                  <id>stop-tomcat</id>
                  <phase>post-integration-test</phase>
                  <goals>
                    <goal>shutdown</goal>
                  </goals>
                </execution>
              </executions>
            </plugin>
            <plugin>



Again, the tomcat7-maven-plugin version can vary. Always check for the latest version in the maven repository.
 In the example above, I changed the port to 9999, to avoid port conflict during start up (it uses 8080 by default ).

Next step is to add/modify your maven-surefire-plugin. We need to separate the integration testing from regular unit testing.

  <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.12.2</version>
                <configuration>
                    <argLine>-Duser.language=en</argLine>
                    <argLine>-Xmx1024m</argLine>
                    <argLine>-XX:MaxPermSize=256m</argLine>
                    <argLine>-Dfile.encoding=UTF-8</argLine>
                    <useFile>false</useFile>
                    <excludes>
                        <exclude>**/*IntegrationTest.java</exclude>
                    </excludes>
                </configuration>
                <executions>
                    <execution>
                        <id>default-test</id>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <phase>test</phase>
                        <configuration>
                            <excludes>
                                <exclude>**/*IntegrationTest.java</exclude>
                            </excludes>
                            <includes>
                                <include>**/*Test.java</include>
                            </includes>
                        </configuration>
                    </execution>
                    <execution>
                        <id>integration-tests</id>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                             <excludes>
                               <exclude>**/*Test.java</exclude>
                             </excludes>
                             <includes>
                               <include>**/*IntegrationTest.java</include>
                             </includes>
                           </configuration>
                    </execution>
                </executions>
            </plugin>


Take note that all of your integration classes should be appended with "IntegrationTest" otherwise, it will not be executed.

Then you should define you maven-failsafe-plugin. The failsafe plugin is designed to run integration tests while the surefire plugins is designed to run unit tests.

                <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-failsafe-plugin</artifactId>
              <version>2.12.4</version>
              <configuration>
                <includes>
                  <include>**/*IntegrationTest*</include>
                </includes>
              </configuration>
              <executions>
                <execution>
                  <goals>
                    <goal>integration-test</goal>
                    <goal>verify</goal>
                  </goals>
                </execution>
              </executions>
            </plugin>



At this point, you can do "mvn clean install" and see if you are getting a successful build. The next step is to define your cucumber entry point class. The entry point is commonly named RunCukes, but since we are running cucumber during integration testing, we need to name the class as RunCukesIntegrationTest.

@RunWith(Cucumber.class)
@Cucumber.Options( glue = {"com.sample.project"} )
public class RunCukesIntegrationTest {
}


In all the examples that I have seen, this class does nothing but implement 2 annotations. The Cucumber.Options can have other parameters, but the important parameter is to define the glue. The glue value tells the package name where you put all your integration testing.

In your test/resources add a text file named student.feature.  This is where you put your cucumber features. You should place the student.feature file inside a package you define in your glue.

student-webapp/src/test/resources/com/sample/project/student.feature

Feature: When you query a student

  Scenario: Find student of a given student id
    Given A studentID 

    When the user of your rest api queries student
    Then returns student resource



The indentation is important. You can learn more about the format of .feature file in this link https://github.com/cucumber/cucumber/wiki/Feature-Introduction

Next is to define your inegration class. By convention with cucumber-jvm, they normally name the class as Stepdefs (e.g. StudentStepdefs) , but you can name it whatever you want.  First, I defined a base class for all my integration test to avoid code duplicity.

@WebAppConfiguration
@ContextConfiguration(locations = {"classpath:cucumber.xml"})
public class BaseIntegration {
    public static final String SERVER_PORT = "9999";
    public static String SERVER_URL = "http://localhost:" + SERVER_PORT + "/student-webapp";

    @Autowired
    protected RestTemplate restTemplate;

}




Since I will be hitting a webapp, I have autowired spring RestTemplate. You also need to define the ContextConfiguration. Cucumber-jvm does not work if you wire you applicationContext directly. You have to define a file called cucumber.xml in your  maven src/test/resources/cucumber.xml, and the content of that file would simply be:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <import resource="classpath:applicationContext.xml"/>
</beans>


The next and final step is to define your integrationTest class that is wired to your .feature file.



public class StudentEndpointIntegrationTest  extends BaseIntegration {


    public static String STUDENT_URL = SERVER_URL + "/student/{studentID}";
    private String studentID;


    @Given("^A studentID$")
    public void givenAStudentId() {
        studentID = "14344";
    }



    @When("^the user of your rest api queries student$")
    public void getStudent() throws Exception {

        Map<String, String> params = new HashMap<String, String>();
        params.put("studentID",  studentID);

        MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
        headers.add("userAuth" , "abc123" );

        HttpEntity<Object> httpEntity = new HttpEntity<Object>( headers );

        ResponseEntity<StudentResponse> responseEntity =  restTemplate.exchange(STUDENT_URL, HttpMethod.GET,httpEntity,StudentResponse.class,params);
        response = responseEntity.getBody();
    }



    @Then("^returns student resource$")
    public void itShouldHaveStudent() {
        assertThat(response.getStudents().size(),is(1));
    }



There are 3 annotations that should be used to match the .feature file. The parameter of these annotations (@Given, @When and @Then) is a string and can take regular expression.

Once your integration test class is defined, you can do "mvn clean install" and see that tomcat is started and your integration test is executed.

Hope this helps!