Testing Spring Boot microservices using Cucumber and CWB REST

In this tutorial I will show you how to use Cucumber and CWB REST to test a Spring Boot microservice.  Starting from a Spring Boot application using an embedded Tomcat, we will first setup the CWB integration and write a simple feature.  The feature will connect to the actual running webserver and execute its scenario's. As a next step we will configure the embedded tomcat to use a dynamic server port and see a simple code based approach for starting the webserver en running the Cucumber features on any port.

This is really a step-by-step tutorial.  It helps if you already know some Spring Boot and Cucumber, but it is not strictly required.  In the first part of this tutorial we'll create the microservice using Spring Boot, in the second part we'll test it with CWB REST.

You will need Java 8, Maven and your favourite IDE... and 15 to 30 minutes of time.

Downloading the source code

If you want to jump ahead, you can find all source code of this sample on https://bitbucket.org/beforeach/cwb-rest-tutorial

Creating the Spring Boot microservice

Setting up the basic project

For our sample we'll create a repository of books that we expose as a REST webservice.  Using Spring Boot, Spring Data JPA and Spring Data REST, we barely have to write any code for this.

To create the basic project structure, configure a basic Spring Boot project on http://start.spring.io with the JPARest repositoriesWeb and H2 starters.  

We end up with a ZIP file containing a project structure like the following:

Creating the domain model

For our demo we will create a simple domain model: a BookRepository that manages Book entities.  We will implement this using Spring Data JPA, storing the books in a JDBC datasource.

Our book is a very simple entity that has a unique id, a title and an author field.

com.foreach.cwb.tutorials.domain.Book
@Entity
public class Book
{
   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;

   private String title, author;

   public Long getId() {
      return id;
   }

   public void setId( Long id ) {
      this.id = id;
   }

   public String getTitle() {
      return title;
   }

   public void setTitle( String title ) {
      this.title = title;
   }

   public String getAuthor() {
      return author;
   }

   public void setAuthor( String author ) {
      this.author = author;
   }
} 

Our BookRepository is nothing but a simple interface extending the Spring Data JpaRepository interface.   We also annotate our repository with RepositoryRestResource.  Because we added the Rest Repositories starter to our boot application, Spring Data REST will automatically create a REST endpoint for this repository, complete with HATEOAS support.

com.foreach.cwb.tutorials.repositories.BookRepository
@RepositoryRestResource(collectionResourceRel = "books", path = "books")
public interface BookRepository extends JpaRepository<Book, Long> {
}

This is already a fully working application with a /books REST endpoint to which we can query and update the collection of Book entities.  Run the application by executing the main method of CwbRestApplication.  This will start an embedded Tomcat on the default port 8080.

A simple CURL request can be used to verify that the books service is up and running:

curl http://localhost:8080/books
-----------------------------------
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/books{?page,size,sort}",
      "templated" : true
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 0,
    "totalPages" : 0,
    "number" : 0
  }
}

Creating default data

As is to be expected a call to GET /books gives no results as we have no books in our repository.  For our tests we want to add some fixed test data, and in this particular case we create some default books when starting the application.  Here we simply add the code to CwbRestApplication.

com.foreach.cwb.tutorials.CwbRestApplication
@SpringBootApplication
public class CwbRestApplication {

    @Autowired
    public void createBooks(BookRepository bookRepository) {
        Book jurassicPark = new Book();
        jurassicPark.setAuthor("Michael Crichton");
        jurassicPark.setTitle("Jurassic Park");

        Book schindlersList = new Book();
        schindlersList.setAuthor("Thomas Keneally");
        schindlersList.setTitle("Schindler's List");

        Book lowExpectations = new Book();
        lowExpectations.setAuthor("Unknown");
        lowExpectations.setTitle("Not So Great Expectations");

        bookRepository.save(Arrays.asList(jurassicPark, schindlersList, lowExpectations));
    }

    public static void main(String[] args) {
        SpringApplication.run(CwbRestApplication.class, args);
    }
}

After restarting the application, GET /books should now return a predictable set of books:

curl http://localhost:8080/books
-----------------------------------
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/books{?page,size,sort}",
      "templated" : true
    }
  },
  "_embedded" : {
    "books" : [ {
      "title" : "Jurassic Park",
      "author" : "Michael Crichton",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/1"
        }
      }
    }, {
      "title" : "Schindler's List",
      "author" : "Thomas Keneally",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/2"
        }
      }
    }, {
      "title" : "Not So Great Expectations",
      "author" : "Unknown",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/3"
        }
      }
    } ]
  },
  "page" : {
    "size" : 20,
    "totalElements" : 3,
    "totalPages" : 1,
    "number" : 0
  }
}

Setting up the project for CWB REST

Our simple microservice is now set up, that's all there is to it.  We want to use CWB REST to test our /books endpoint, to be able to do that we first need to setup the project.  This involves some fairly straightforward steps that only need to happen once.

Updating pom.xml with the required dependencies

First we add the required cwb-rest dependency to the pom.xml:

<dependency>
   <groupId>com.foreach.cwb</groupId>
   <artifactId>cwb-rest</artifactId>
   <version>1.1.0.RELEASE</version>
   <scope>test</scope>
</dependency>

The current version of CWB REST (1.1.0.RELEASE) is not compatible with JUnit versions higher than 4.11. To avoid conflicts we need to force the JUnit version to 4.11 using the dependencyManagement section of the pom.xml.

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>4.11</version>
      </dependency>
   </dependencies>
</dependencyManagement>
Full source code of pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>com.foreach.cwb.tutorials</groupId>
    <artifactId>cwb-rest</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>cwb-rest</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
        <relativePath/>
        <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.11</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.foreach.cwb</groupId>
            <artifactId>cwb-rest</artifactId>
            <version>1.1.0.RELEASE</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Adding cucumber.xml and the resources

When running CWB features the runner will look for a cucumber.xml file in the resources classpath.  This Spring configuration xml is required to hook up all CWB related beans for test steps.  Simply copy the following content to your test resources:

Source code: test\resources\cucumber.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
   <bean id="cwbProperties" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
      <property name="locations">
         <list>
            <value>classpath:/cwb.properties</value>
         </list>
      </property>
      <property name="ignoreUnresolvablePlaceholders" value="true"/>
      <property name="ignoreResourceNotFound" value="true"/>
   </bean>

   <bean id="dataProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
      <property name="locations">
         <list>
            <value>classpath:/properties/default.properties</value>
            <value>classpath:/properties/${environment}.properties</value>
         </list>
      </property>
      <property name="ignoreResourceNotFound" value="true"/>
   </bean>

   <context:annotation-config/>
   <context:component-scan base-package="com.foreach.cuke"/>
</beans>

The cucumber.xml configures 2 properties beans:

  • cwbProperties allows you to specify the master configuration that CWB uses when executing
  • dataProperties contain the test/environment related properties you want to use in your features

Both beans can specify any number of property sources, but all are optional.  For this example we will configure some dataProperties and set the property rest.base.url.  Create a properties file in the location specified in the cucumber.xml.

Source code: test\resources\properties\default.properties
# Configure the default base url for all relative paths
rest.base.url=http://localhost:8080

Setting the rest.base.url property will ensure that all relative calls will get prefixed.  Calls to /books will automatically be translated to http://localhost:8080/books.  This will improve feature readability but (as we will see) also makes it easy to change the target host.

Adding a feature and JUnit test

Having set up the basic CWB configuration, we can add a unit test class that should execute Cucumber features, and add a basic feature file.

We add the following test class:

Source code: com.foreach.cwb.tutorials.TestCwbRestFeatures
@RunWith(Cucumber.class)
@CucumberOptions(
      features = { "classpath:features/" },
      glue = "com.foreach.cuke",
      format = {
            "json:target/cucumber/cucumber-report.json",
            "html:target/cucumber/plain-html-reports",
            "com.foreach.cuke.core.formatter.ConsoleReporter"
      }
)
public class TestCwbRestFeatures
{
}

Without digging into the specifics, the annotation will look for Cucumber features present in the features folder on the classpath and will execute them using CWB support (com.foreach.cuke glue).

All that's left to do is to add a simple feature:

Source code: test\resources\features\my.feature
Feature: Books repository

  Scenario: Fetching the list of books
    * i call get "/books"

We've now reached a point of being able to execute the Cucumber features using CWB REST.  If you start the CwbRestApplication first and then in a separate process execute the TestCwbRestFeatures unit test, it should work.  This is already an excellent way of working if you for example want to start the application using Maven, then execute all unit tests and then shut it down.  

An alternative however is to start the application from within the unit test.  Seeing as our microservice comes with an embedded Tomcat, this can be done very easily.

Starting the webserver from within the unit test

To be able to start the webserver from the unit test we only need to call the CwbRestApplication main method from our unit test.  We can easily hook this up through the cucumber.xml file.  The cucumber.xml contains the Spring ApplicationContext configuration for running the CWB features.  This ApplicationContext is started a single time before all features are executed, this makes it an excellent location to add a call to the CwbRestApplication.

In the cucumber.xml add a MethodInvokingBean that will execute CwbRestApplication#main when being created:

XML bean configuration to add to cucumber.xml
<bean id="startTomcat" class="org.springframework.beans.factory.config.MethodInvokingBean">
   <property name="staticMethod" value="com.foreach.cwb.tutorials.CwbRestApplication.main" />
</bean>

At this point you can shut down any running application instance you might still have.  If you now execute TestWebRestFeatures you will see the Spring Boot banner show up in your output, meaning the application and embedded Tomcat are started before feature execution.  If you configured everything the right way, simply executing the unit test should yield a green result.

A real scenario: verifying the configured books

Actually our single-line scenario does quite a lot right now.  When running the test:

  • an embedded Tomcat is started on port 8080
  • the Spring Boot microservice application is loaded (and the books are created)
  • a call to http://localhost:8080/books is being made by CWB REST code
  • books are fetched from the H2 database and returned to the caller

We're executing the full application stack... but of course we are not testing very much.

In order to give you a more real-world feel of CWB scenarios, change the scenario implementation to the following:

Source code: test\resources\features\my.feature
Feature: Books repository

  Scenario: Two default books should always be returned
    When i call get "/books"
    Then the response status should be 200
    And response entity "_embedded.books" should contain at least 2 entities
    And the response should contain data:
    """
      _embedded.books:
        - title: Jurassic Park
          author: Michael Crichton
        - title: Schindler's List
          author: Thomas Keneally
    """

The scenario should be pretty self-explanatory.  We validate that 2 of the known books are returned in the response, and in this case we use some YAML to define part of the expected response.

If you want proof that the scenario is being executed, simply change any of the parameters (status code, number of entities, response data) to something invalid and see the test fail.

Using a dynamic server.port

In the current setup the webserver is started on port 8080, if something else is already running on that port, the application will not start.  We can easily configure Spring Boot to look for an available port and start the webserver on that port.  All we have to do is set the server.port property to 0 in the application.properties

Source code: main\resources\application.properties
server.port=0

However our Cucumber tests will still connect to http://localhost:8080 and now fail.  To fix this, I will show you a different way to start the application, retrieve the server port the webserver is using and pass it to the data properties.

We will extend TestCwbRestFeatures with the following code:

  • start the Spring Boot application
  • retrieve the started EmbeddedWebApplicationContext and get the server port from the servlet container
  • modify the CWB dataProperties and replace the server port in the rest.base.url

 

Source code: com.foreach.cwb.tutorials.rest.TestCwbRestFeatures
@RunWith(Cucumber.class)
@CucumberOptions(
      features = { "classpath:features/" },
      glue = "com.foreach.cuke",
      format = {
            "json:target/cucumber/cucumber-report.json",
            "html:target/cucumber/plain-html-reports",
            "com.foreach.cuke.core.formatter.ConsoleReporter"
      }
)
public class TestCwbRestFeatures
{
   @Autowired
   @Qualifier("dataProperties")
   private Properties dataProperties;

   @PostConstruct
   public void bootApplication() {
      EmbeddedWebApplicationContext webApplicationContext
            = (EmbeddedWebApplicationContext) SpringApplication.run( CwbRestTutorialApplication.class );

      // retrieve the actual webserver port
      int webServerPort = webApplicationContext.getEmbeddedServletContainer().getPort();

      // modify the rest.base.url, replace the default port by the actual
      String restBaseUrl = dataProperties.get( "rest.base.url" ).toString();
      dataProperties.put( "rest.base.url", StringUtils.replace( restBaseUrl, "8080", "" + webServerPort ) );
   }
}

Now all that's left to do is to remove the MethodInvokingBean from the cucumber.xml and replace it with a bean of type TestCwbRestFeatures:

Bean definition to add to cucumber.xml
<bean id="startApplication" class="com.foreach.cwb.tutorials.TestCwbRestFeatures" />

Without having to change any of the features, we now start up the application on a random port and connect to it.

Another scenario: adding a book

This about wraps up this little tutorial, but before signing off, let's add another scenario to our feature:

Scenario for creating a book
Scenario: A newly created book should be added to the list
  Given i call post "/books" with data
    """
      title: "My book: #{id.scenario}"
      author: Myself
    """
  And the response status is 201
  When i call get "/books"
  Then the response entity "_embedded.books[]" should contain:
    | title  | My book: #{id.scenario} |
    | author | Myself                  |

This scenario showcases some other features of CWB REST: posting data using YAML and using a table syntax to find an item in an array.

 

As I hope to have illustrated, setting up CWB Rest to test Spring Boot microservices is fairly easy.  In this tutorial I showed the absolute basics, in a follow-up I'll extend this sample and show how you can test REST webservices secured with basic or OAuth2 authentication.