How to Set Up a CI Build for Integration Tests on Jenkins

JAN 09

A Continuous Integration (CI) build with a thorough test suite allows you to be confident that your code changes do not break existing functionality. This helps you avoid introducing bugs which enables you to develop features faster.

A thorough test suite consists of unit tests and integration tests.

Unit tests verify that a small block of code behaves as expected under a well-defined set of external conditions. Unit tests aim to isolate the code under test.

Integration tests exercise the interactions between different components of a system. As such, integration tests often require a testing database. While many software engineers use automated unit tests as part of their standard development practices, they tend to be less consistent about writing automated integration tests.

In this blog post, I will show you how to set up an integration testing environment using a Jenkins CI server and an AWS DynamoDB database. We will set up AWS DynamoDB Local on Jenkins and will populate this database with test data once for the entire test suite. Our tests will be written in Java and compiled with maven.

Start with why

At Twilio, we always say start with why. So before we get started, why do you want to set up a local testing instance of your database? Can we accomplish our testing goals by mocking out the database?

We decided to set up an automated integration testing environment so that we can be confident that the components of our system continue to communicate correctly with each commit. We use an instance of AWS DynamoDB deployed on a Jenkins CI server so that our testing conditions closely reflect our production environment.

Code Layout

my-parent-project/ 
  my-project/ 
    src/ 
      main/ 
        java/
          com.twilio.project/ 
            MyProjectToTest.java 
      test/ 
        java/ 
          com.twilio.project/
            DynamoDbTest.java 
            DynamoDBTestInitializer.java 
            MyFirstDynamoDbTest.java
            MyMoreComplicatedTest.java 
    scripts/ 
      DynamoDBLocal/ 
        DynamoDBLocal_lib/
          dynamodb-dependency1.java 
          dynamodb-dependency2.java 
          third_party_licenses/
            license1.java 
            license2.java 
      portscan 
      start_dynamo.sh 
      stop_dynamo.sh
      my_yaml_file.yaml 

Installing a local instance of AWS DynamoDB on your Jenkins CI server

Download AWS DynamoDB and place all jars and licenses in a folder according to the layout above.

Choosing a database port

The following script creates a new socket and returns the socket’s address.
This address is the port we assign to AWS DynamoDB. See the socket module documentation for more information.

We run the script and assign the output to an environment variable DYNAMODB_PORT.

DYNAMODB_PORT='./portscan'

portscan
#!/usr/bin/env python import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', 0))
addr = s.getsockname() print addr[1] s.close() 

Start and Stop an AWS DynamoDB Instance on the chosen database port

The tests MyFirstTest.java and AnotherTest.java require an instance of AWS DynamoDB to be running in order to pass. In this section, I start an AWS DynamoDB server, build my project, which runs my tests, and then stop the AWS DynamoDB server. My configuration places the following commands in a yaml file.

To Start AWS DynamoDB: run ./start_dynamo.sh $DYNAMODB_PORT

start_dynamo.sh
#!/bin/bash

    set -x

    PORT=$1 
    pushd DynamoDBLocal 
    java -mx512m -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -port $PORT 2>&1  &> dynamo.log popd echo $PORT 

Now I can build my project with this maven command.
mvn -Daws.accessKeyId=aws_key -Daws.secretKey=aws_secret clean package

And once the build completes, stop the AWS DynamoDB instance: ./stop_dynamo.sh $DYNAMODB_PORT

stop_dynamo.sh
#!/bin/bash

    set -e set -x

    PORT=$1 kill -int `netstat -nap | grep LISTEN | grep :$PORT | awk {'print $7'} | sed 's/\/.*//'` 

set -e forces the bash script to fail immediately when any command returns an non-zero exit code.

set -x prints commands and their arguments as they are executed in the script.

kill -int kills the running process and allows java to clean up resources.

netstat -nap | grep LISTEN | grep :$PORT displays the process that is listening on $PORT.

Then awk {'print $7'} | sed 's/\/.*//' parses the full process entry to get the process id.

For more information, go to the netstat documentation.

Configure AWS DynamoDB in your java project Remember that we set the

DYNAMODB_PORT environment variable above. This allows us to dynamically set and use a port available on our Jenkins server. We read in this value in the java configuration using System.getenv("DYNAMODB_PORT"):


DynamoDBConfiguration.java
public class TwilioDynamoDBConfiguration {

        public int port = Optional.ofNullable(System.getenv("DYNAMODB_PORT")).map(Integer::parseInt).orElse(443);
        ... 
} 

Populate your AWS DynamoDB database with test data

Now that we have the correct AWS DynamoDB port, we can initialize the AWS DynamoDB database. The first test has the following setUp() method where initializer.setup() is responsible for populating the AWS DynamoDB database with test data. We place the @BeforeClass annotation before the first test in the test suite so that we only need to initialize the data for AWS DynamoDB once. We choose @BeforeClass instead of @Before because @BeforeClass runs only once, independendent of the number of @Test methods in a class, whereas@Before runs once before each test in a class. We only populate our database once because this operation is expensive.


DynamoDbTest.java
import org.junit.BeforeClass;

public class DynamoDbTest { 
    @BeforeClass 
    public static void initializeDynamo() { 
        DynamoDBTestInitializer initializer = DynamoDBTestInitializer.getInstance(); 
        initializer.setup();

        this.additionalDynamoInitialization(); 
    }

    public static void additionalDynamoInitialization() { } 
} 


MyFirstDynamoDbTest.java
import org.junit.Test;

public class MyFirstDynamoDbTest extends DynamoDbTest { 

    @Test 
    public void testCanViewItemA() { 

      Response response = httpGet("A");
      assertThat(response.getStatus()).isEqualTo(200); 
      String body = response.readEntity(String.class); 
      assertThat(body.contains("Item A"));
    }

    @Test 
    public void testCanViewItemB(){ ...  } 
} 


MyMoreComplicatedTest.java
import org.junit.Test;

class MyMoreComplicatedTest extends DynamoDbTest {

   @override 
   public static void additionalDynamoInitialization() {
     // Add some more fixtures to dynamo
   }

   @Test 
   public void testSomethingComplex() {
      // use the fixtures in your assertions
   } 
} 


DynamoDBTestInitializer.java
public class DynamoDBTestInitializer{

    public static void setup(){ 
      createSpecialItemA(); 
      createItemB(); 
    }

    /*
    * the call to httpPost writes data to the database through our
    * application's API
    */
    private static void createItemA(){ 
      Form form = new Form();
      form.param("Item A", "Item of type A"); 
      form.param("Special Description", "More special details on item A");

      Response response = httpPost("A", form);
      assertThat(response.getStatus()).isEqualTo(201); 
    }

    private static void createItemB(){ 
      Form form = new Form();
      form.param("Item B", "Item of type B"); 
      form.param("Description", "More details on item B");

      Response response = httpPost("B", form);
      assertThat(response.getStatus()).isEqualTo(201); 
    } 
} 

We have setup an automated integration testing environment with AWS DynamoDB and Jenkins. Now we can write integration tests which give us confidence that the components of our system interact correctly, and will continue to do so as we write new code.

Image of Coverage Report

Posted by Cara Borenstein on January 09, 2017