Testing Among the Clouds, Part 2

In a recent post I wrote about the particular problems we’ve been having with integration testing the Spring LDAP project and the use we’ve made of Amazon EC2 for solving these problems. In this post I’ll present the implementation details.

Prerequisites

In order to keep this reasonably brief I’ll have to refer to the getting started guide for information on how to get going with Amazon EC2.

A FactoryBean to Launch EC2 Instances

What we want to achieve here is to launch an EC2 instance transparently and independently of the actual test code. Ideally, the test code should be oblivious of the target server and we want to externalize those details to external configuration. We will use Spring’s excellent JUnit test support to automatically wire the integration test setup and make sure that the target server is up and running before the actual test code is running.

A powerful way to transparently perform complex initialization logic when using Spring is the FactoryBean concept. We will create a FactoryBean implementation to launch the EC2 image and set up our target resource:

AbstractEc2InstanceLaunchingFactoryBean.java
public abstract class AbstractEc2InstanceLaunchingFactoryBean extends AbstractFactoryBean {
  private static final int INSTANCE_START_SLEEP_TIME = 1000;
  private static final long DEFAULT_PREPARATION_SLEEP_TIME = 30000;
  private static final Log log = LogFactory.getLog(AbstractEc2InstanceLaunchingFactoryBean.class);
  private String imageName;
  private String awsKey;
  private String awsSecretKey;
  private String keypairName;
  private String groupName;
  private Instance instance;
  private long preparationSleepTime = DEFAULT_PREPARATION_SLEEP_TIME;
  public void setImageName(String imageName) {
      this.imageName = imageName;
  }
  public void setAwsKey(String awsKey) {
      this.awsKey = awsKey;
  }
  public void setAwsSecretKey(String awsSecretKey) {
      this.awsSecretKey = awsSecretKey;
  }
  public void setKeypairName(String keypairName) {
      this.keypairName = keypairName;
  }
  public void setGroupName(String groupName) {
      this.groupName = groupName;
  }
  public void setPreparationSleepTime(long preparationSleepTime) {
    this.preparationSleepTime = preparationSleepTime;
  }
  @Override
  protected final Object createInstance() throws Exception {
      Assert.hasLength(imageName, "ImageName must be set");
      Assert.hasLength(awsKey, "AwsKey must be set");
      Assert.hasLength(awsSecretKey, "AwsSecretKey must be set");
      Assert.hasLength(keypairName, "KeyName must be set");
      Assert.hasLength(groupName, "GroupName must be set");

      log.info("Launching EC2 instance for image: " + imageName);

      Jec2 jec2 = new Jec2(awsKey, awsSecretKey);
      LaunchConfiguration launchConfiguration = new LaunchConfiguration(imageName);
      launchConfiguration.setKeyName(keypairName);
      launchConfiguration.setSecurityGroup(Collections.singletonList(groupName));

      ReservationDescription reservationDescription = jec2.runInstances(launchConfiguration);
      instance = reservationDescription.getInstances().get(0);
      while (!instance.isRunning() && !instance.isTerminated()) {
          log.info("Instance still starting up; sleeping " + INSTANCE_START_SLEEP_TIME + "ms");
          Thread.sleep(INSTANCE_START_SLEEP_TIME);
          reservationDescription = jec2.describeInstances(Collections.singletonList(instance.getInstanceId())).get(0);
          instance = reservationDescription.getInstances().get(0);
      }

      if (instance.isRunning()) {
          log.info("EC2 instance is now running");
          if (preparationSleepTime > 0) {
              log.info("Sleeping " + preparationSleepTime + "ms allowing instance services to start up properly.");
              Thread.sleep(preparationSleepTime);
              log.info("Instance prepared - proceeding");
          }
          return doCreateInstance(instance.getDnsName());
      } else {
          throw new IllegalStateException("Failed to start a new instance");
      }
   }

  protected abstract Object doCreateInstance(String ip) throws Exception;

  @Override
  protected void destroyInstance(Object ignored) throws Exception {
    if (this.instance != null) {
      log.info("Shutting down instance");
      Jec2 jec2 = new Jec2(awsKey, awsSecretKey);
      jec2.terminateInstances(Collections.singletonList(this.instance.getInstanceId()));
    }
  }
}

This superclass makes use of the typica library (also available in maven) to launch the EC2 instance and delegates to doCreateInstance for creating the target resource that we will use in our test case. In our setup we want to create a ContextSource, which is the Spring LDAP equivalent of a DataSource:

ContextSourceEc2InstanceLaunchingFactoryBean.java
public class ContextSourceEc2InstanceLaunchingFactoryBean extends AbstractEc2InstanceLaunchingFactoryBean {
  private String base;
  private String userDn;
  private String password;
  @Override
  public final Class getObjectType() {
    return ContextSource.class;
  }
  @Override
  protected final Object doCreateInstance(final String dnsName) throws Exception {
    Assert.hasText(userDn);
    LdapContextSource instance = new LdapContextSource();
    instance.setUrl("ldap://" + dnsName);
    instance.setUserDn(userDn);
    instance.setPassword(password);
    instance.setBase(base);

    instance.afterPropertiesSet();
    return instance;
  }
  public void setBase(String base) {
    this.base = base;
  }
  public void setUserDn(String userDn) {
    this.userDn = userDn;
  }
  public void setPassword(String password) {
    this.password = password;
  }
}

Note that the AbstractEc2InstanceLaunchingFactoryBean will wait for the instance to start up properly. It will also wait an additional period of time once the instance is up and running to allow for all relevant services to start up properly. Finally it will automatically close down the launched instance once it is properly shut down (which it will be when executing using the Spring automated test support).

Test Case Configuration

We now have the infrastructure to have the EC2 instance launched transparently from a Spring Application Context. All we need to do is configure it:

testContext.xml

  
  



  
  
  
  
  
  
  
  



  

We are referring to ldap.properties for the actual settings:

userDn=cn=admin,dc=jayway,dc=se
password=secret
base=dc=jayway,dc=se
aws.ami=ami-56ec083f
aws.keypair=spring-ldap-keypair
aws.security.group=spring-ldap

The referenced AMI is a public image specifically set up for our test cases. It has OpenLDAP installed and has been pre-populated with the test data that our test cases expect.

Note that we are not specifying any property value for aws.key and aws.secret.key, as this is the Amazon account access information. That will be the account settings of the individual developer and should be supplied at runtime using system properties (e.g. mvn -Daws.key=xxxxxxx -Daws.secret.key=xxxxxx test).

Performing the Test

Now all we need to do is use Spring’s automated test support to start our test:

SomeLdapITest.java
@ContextConfiguration(locations = { "testContext.xml" })
public class SomeLdapITest extends AbstractJUnit4SpringContextTests {
  @Autowired
  private LdapTemplate tested;

  @Test
  public void testSomething() {
    // Use the automatically injected LdapTemplate instance to perform a test.
  }
}

Set to Go

With the above you should be all set to go to start using this powerful approach to integration testing. The actual code is available with the 1.3.0.RC1 release of Spring LDAP; links to downloads and documentation available here.

Leave a Reply

Close Menu