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.