Putting OSGi to the Test with Pax Exam
Join the DZone community and get the full member experience.
Join For FreeAfter a small delay, I finally bring you the next installment in my series of OSGi-oriented articles. This time we're going to have a look at writing tests around OSGi bundles.
It should go without saying that testing is an important part of software development. Developer-driven tests are often based on JUnit and usually focus on testing application units in isolation. But it's just as important to write integration tests to ensure that those units play well together. With regard to OSGi, it's important to write tests that put one or more bundles in an OSGi runtime to make sure that those bundles behave as expected.
I made the claim a few months ago that testing OSGi is quite easy. Today I'm going to show that to be true by showing you Pax Exam.
Pax Exam is a testing toolkit that addresses the need for bundle-level testing. What's particularly interesting about Pax Exam is how it works. When a Pax Exam test is run, it starts an OSGi framework, installs and starts a selection of bundles, and then makes an OSGi BundleContext available through which you can make assertions about your bundles, the services that they publish, and the effects that they have on each other.
Before we get started, you should know that I've reworked the previous article's Pig Latin translator project quite a bit. The new version is about translator services in general, of which Pig Latin is one implementation. Some package names have changed and it's now a Pax Construct-based project. I'm not going to go over the new code here, but it should be familiar enough that you should be able figure it out. Download the example code to follow along.
To get started with Pax Exam, we're going to create a separate project to house our bundle tests. There are a lot of ways to do this, but knowing that our bundle-test project is only going to contain a Maven pom.xml file and a single test class, I find it easy enough to take advantage of the Unix mkdir command and its -p option:
translators% mkdir -p bundle-tests-exam/src/test/java/com/habuma/translator/test
That sets up all of the directory structure we need. To fill it out, we're going to place a pom.xml file in the bundle-tests-exam directory and a test class in the bottom level test directory. (Sorry Windows users...I don't think your mkdir has an equivalent of the -p, so you'll have to create the directory structure manually.)
Okay, so let's create the pom.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.habuma.translator</groupId>
<artifactId>bundle-tests-exam</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.habuma.translator</groupId>
<artifactId>interface</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.exam</groupId>
<artifactId>pax-exam</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.exam</groupId>
<artifactId>pax-exam-container-default</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.ops4j.pax.exam</groupId>
<artifactId>pax-exam-junit</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.5</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
There are several things I'd like to highlight in this pom.xml:
- First notice that I set the compiler plugin to use Java 1.5. Pax Exam is based on JUnit 4, which itself is based on Java 1.5 annotations. This plugin configuration ensures that Maven won't choke when it sees @Test and @RunWith annotations in our test class.
- There are five dependencies, the first of which is the translator interface bundle. Our test class isn't going to directly use the Pig Latin service implementation, but it will work with the Translator interface, so we'll need this bundle available
- The next 3 dependencies are Pax Exam itself.
- The final dependency is JUnit 4.5.
With the Maven setup out of the way, we're now ready to write our test class. PigLatinTranslatorBundleTest uses Pax Exam's JUnit4TestRunner to run a series of OSGi bundle tests.
package com.habuma.translator.test;
import static org.junit.Assert.*;
import static org.ops4j.pax.exam.CoreOptions.*;
import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.*;
import java.util.List;
import java.util.Properties;
import org.junit.Test;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.Inject;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.junit.Configuration;
import org.ops4j.pax.exam.junit.JUnit4TestRunner;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import com.habuma.translator.Translator;
@RunWith(JUnit4TestRunner.class)
public class PigLatinTranslatorBundleTest {
@Inject
private BundleContext bundleContext;
private Translator translator;
@Before
public void setup() throws Exception {
translator = retrievePigLatinService();
}
@Configuration
public static Option[] configuration()
{
return options(equinox(), profile("spring.dm"), provision(
mavenBundle().groupId("com.habuma.translator").artifactId("interface"),
mavenBundle().groupId("com.habuma.translator").artifactId("pig-latin")
));
}
// ... Test methods go here ...
private Translator retrievePigLatinService() throws InterruptedException {
ServiceTracker tracker = new ServiceTracker(bundleContext,
Translator.class.getName(), null);
tracker.open();
Translator pigLatinService = (Translator) tracker.waitForService(5000);
tracker.close();
assertNotNull(pigLatinService);
return pigLatinService;
}
}
Probably the most interesting piece of PigLatinTranslatorBundleTests is the configuration() method. It is annotated with @Configuration to indicate that this method will return an array of Pax Exam options--effectively configuring the Pax Exam test case.
Within the configuration() method, I use a set of static methods provided by Pax Exam's CoreOptions and PaxRunnerOptions classes. Specifically, I ask Pax Exam to run the tests within the latest version of Equinox, using Pax Runner's Spring-DM profile. As for the bundles I want to test, I ask it to install the translator project's inteface and pig-latin bundles.
When the Pax Exam test starts up, the first thing it will do is start an OSGi framework (in this case, the latest version of Equinox), install the bundles specified by the Spring-DM profile, and then install our bundles that we want to test. At this point, the OSGi runtime is running, loaded, and ready to roll.
But before we can test our Pig Latin bundle, we're going to need a reference to the Pig Latin service and to the BundleContext. To accomodate that, Pax Exam offers an @Inject annotation to automatically provide the BundleContext to the test. With the BundleContext in hand, the setup() method calls retrievePigLatinService() to lookup the service.
Now we're ready to start testing our bundles. The first thing we should do is make sure that the BundleContext is available. If anything goes wrong while starting the OSGi runtime, we won't have a BundleContext to work with and there'd be no point in testing anything else. So, let's write a test that simply asserts that the bundleContext variable is not null:
@Test
public void bundleContextShouldNotBeNull() throws Exception {
assertNotNull(bundleContext);
}
Great! Assuming that the BundleContext is available, we can now do something more interesting, such as testing that we get a service that implements the Translator interface that we expect:
@Test
public void serviceReferenceShouldExist() {
ServiceReference serviceReference =
bundleContext.getServiceReference(Translator.class.getName());
assertNotNull(serviceReference);
assertEquals("Pig Latin",
serviceReference.getProperty("translator.language"));
}
Here, I'm looking up a service reference for the Translator interface and asserting that the service has been registered with its translator.language property set to "Pig Latin".
If that test passes, then we know that the OSGi runtime is up and running and that there's a service that claims to implement the Translator interface. Finally, let's write one more test just to see that the service does what we think it should do:
@Test
public void shouldTranslateText() {
assertNotNull(translator);
assertEquals("id-DAY is-thAY ork-wAY",
translator.translate("Did this work"));
}
This test doesn't have to be a comprehensive test of the translator's abilities--there should be a unit-test that exercises every corner of the test. This test is just a simple smoke test to be sure that we get a service that meets our expectations.
Testing with different OSGi runtimes
One of the things that makes Pax Exam so powerful is that it's very flexible and can be configured to test bundles using virtually any OSGi frameworks. Under the covers, Pax Exam uses Pax Runner to start up the OSGi runtime, so a Pax Exam-based test can run within pretty much any setup that Pax Runner can provide.
Up until now, we've focused our test on running within the latest version of Equinox. That's what the equinox() option is for. But let's suppose that we want to test with the latest version of Felix instead. No problem, just change the configuration() method:
@Configuration
public static Option[] configuration()
{
return options(felix(), profile("spring.dm"), provision(
mavenBundle().groupId("com.habuma.translator").artifactId("interface"),
mavenBundle().groupId("com.habuma.translator").artifactId("pig-latin")
));
}
Testing with either Equinox or Felix is nice, but it might be helpful to test in the latest version of both Felix and Equinox:
@Configuration
public static Option[] configuration()
{
return options(equinox(), felix(), profile("spring.dm"), provision(
mavenBundle().groupId("com.habuma.translator").artifactId("interface"),
mavenBundle().groupId("com.habuma.translator").artifactId("pig-latin")
));
}
Given this arrangement of OSGi frameworks, the three test methods will be executed twice--once for Equinox and once again for Felix.
Maybe the latest version of the framework(s) isn't what you need to test. Maybe you want to hand select a specific version. How about Equinox 3.4.2 and Knopflerfish 2.3.1?
@Configuration
public static Option[] configuration()
{
return options(equinox().version("3.4.2"), knopflerfish().version("2.3.1"),
profile("spring.dm"), provision(
mavenBundle().groupId("com.habuma.translator").artifactId("interface"),
mavenBundle().groupId("com.habuma.translator").artifactId("pig-latin")
));
}
Or maybe you don't want to choose. Maybe you need to know that your bundles work equally well regardless of the OSGi framework that they're installed into. In that case, we might as well test it against all versions of all OSGi frameworks:
@Configuration
public static Option[] configuration()
{
return options(allFrameworksVersions(), profile("spring.dm"), provision(
mavenBundle().groupId("com.habuma.translator").artifactId("interface"),
mavenBundle().groupId("com.habuma.translator").artifactId("pig-latin")
));
}
When I ran the test against all version of all frameworks, the tests were run against 29 varieties of OSGi runtimes in about four and a half minutes. Maybe that's overkill for your needs and all you need is to know that it works in the latest version of all of the popular OSGi runtimes. In that case, allFrameworks() is the way to go:
@Configuration
public static Option[] configuration()
{
return options(allFrameworks(), profile("spring.dm"), provision(
mavenBundle().groupId("com.habuma.translator").artifactId("interface"),
mavenBundle().groupId("com.habuma.translator").artifactId("pig-latin")
));
}
Or maybe you don't care about Knopflerfish or Felix...but you need to know for certain that it works for all versions of Equinox:
@Configuration
public static Option[] configuration()
{
return options(allEquinoxVersions(), profile("spring.dm"), provision(
mavenBundle().groupId("com.habuma.translator").artifactId("interface"),
mavenBundle().groupId("com.habuma.translator").artifactId("pig-latin")
));
}
(Or, if you'd prefer, allFelixVersions() or allKnopflerfishVersions().)
As you can see, you can test your bundles within almost any OSGi runtime. But what about provisioning? Do you have to provision all of the bundles from Maven?
Provisioning options
Given that our bundles are built with Maven, provisioning them from the Maven repository is awful convenient. But it's not the only way to provision bundles for a test. If you'd rather configure a bundle from an HTTP URL, then no problem:
@Configuration
public static Option[] configuration()
{
return options(equinox(), profile("spring.dm"), provision(
bundle("http://www.habuma.com/osgi/bundles/translator-interface.jar"),
mavenBundle().groupId("com.habuma.translator").artifactId("pig-latin")
));
}
Or perhaps you'd like to pull in a bundle from the filesystem:
@Configuration
public static Option[] configuration()
{
return options(equinox(), profile("spring.dm"), provision(
bundle("http://www.habuma.com/osgi/bundles/translator-interface.jar"),
bundle("file:/Users/wallsc/bundles/pig-latin-translator.jar")
));
}
By now you should have a strong appreciation for what Pax Exam brings to the table. Using Pax Exam, you can test a selection of bundles within the OSGi framework(s) of your choice in a relatively tight JUnit test. We've seen how to create a test that asserts some basic expectations about our bundles and that acts as a service consumer to assert that the service works as we'd like.
Next time I plan to show you how Spring-DM supports bundle testing. We'll see how Spring-DM comes with testing support that resembles Pax Exam in many ways, but that has its own special Spring twist. And I promise not to let 2-3 months pass between now and that next blog entry.
Opinions expressed by DZone contributors are their own.
Comments