Chapter 7. Testing OSGi based Applications

By following best practices and using the Spring Dynamic Modules support, your bean classes should be easy to unit test as they will have no hard dependencies on OSGi, and the few OSGi APIs that you may interact with (such as BundleContext) are interface-based and easy to mock. Whether you want to do unit testing or integration testing, Spring-DM can ease your task.

7.1. OSGi Mocks

Even though most OSGi API are interfaces and creating mocks using a specialized library like EasyMock is fairly simple, in practice the amount of code of setting the code (especially on JDK 1.4) becomes cumbersome. To keep the tests short and concise, Spring-DM provides OSGi mocks under org.springframework.osgi.mock package.

It's up to you to decide whether they are useful or not however, we make extensive use of them inside Spring-DM test suite. Below you can find a code snippet that you are likely to encounter in our code base:

private ServiceReference reference;
private BundleContext bundleContext;
private Object service;
    
protected void setUp() throws Exception {
	reference = new MockServiceReference();
	bundleContext = new MockBundleContext() {

		public ServiceReference getServiceReference(String clazz) {
			return reference;
		}

		public ServiceReference[] getServiceReferences(String clazz, String filter) 
				throws InvalidSyntaxException {
			return new ServiceReference[] { reference };
		}
		
		public Object getService(ServiceReference ref) {
		    if (reference == ref)
		       return service;
		    super.getService(ref);
		}
	};

	...
}
	
public void testComponent() throws Exception {
    OsgiComponent comp = new OsgiComponent(bundleContext);
    
    assertSame(reference, comp.getReference());
    assertSame(object, comp.getTarget());
}

As ending words, experiment with them and choose whatever style or library you feel most confortable with. In our test suite we use the aforementioned mocks, EasyMock library and plenty of integration testing (see below).

7.2. Integration Testing

In a restricted environment such as OSGi, it's important to test the visibility and versioning of your classes, the manifests or how your bundles interact with each other (just to name a few).

To ease integration testing, the Spring Dynamic Modules project provides a test class hierarchy (based on org.springframework.osgi.test.AbstractOsgiTests) that provides support for writing regular JUnit test cases that are then automatically executed in an OSGi environment.

In general, the scenario supported by Spring-DM testing framework is:

  • start the OSGi framework (Equinox, Knopflerfish, Felix)

  • install and start any specified bundles required for the test

  • package the test case itself into a on the fly bundle, generate the manifest (if none is provided) and install it in the OSGi framework

  • execute the test case inside the OSGi framework

  • shut down the framework

  • passes the test results back to the originating test case instance that is running outside of OSGi

By following this sequence it is trivial to write JUnit-based integration tests for OSGi and have them integration into any environment (IDE, build (ant, maven), etc.) that can work with JUnit.

The rest of this chapter details (with examples) the features offered by Spring-DM testing suite.

7.2.1. Creating a simple OSGi integration test

While the testing framework contains several classes that offer specific features, it is most likely that your test cases will extend org.springframework.osgi.test.AbstractConfigurableBundleCreatorTests (at least this is what we use in practice).

Let's extend this class and interact with the OSGi platform through the bundleContext field:

public class SimpleOsgiTest extends AbstractConfigurableBundleCreatorTests {

public void testOsgiPlatformStarts() throws Exception {
	System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_VENDOR));
	System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_VERSION));
	System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT));
}
}

Simply execute the test as you normally do with any JUnit test. On Equinox 3.2.x, the output is similar to:

Eclipse
1.3.0
OSGi/Minimum-1.0,OSGi/Minimum-1.1,JRE-1.1,J2SE-1.2,J2SE-1.3,J2SE-1.4}

It's likely that you will see other log statements made by the testing framework during your test execution by these can be disabled and have only an informative value as they don't affect your test execution.

Note that you did not have to create any bundle, write any MANIFEST or bother with imports or exports, let alone starting and shutting down the OSGi platform. The testing framework takes care of these automatically when the test is executed.

Let's do some quering and figure out what the environment in which the tests run is. A simple way to do that is to query the BundleContext for the installed bundles:

public void testOsgiEnvironment() throws Exception {
	Bundle[] bundles = bundleContext.getBundles();
	for (int i = 0; i < bundles.length; i++) {
		System.out.print(OsgiStringUtils.nullSafeName(bundles[i]));
		System.out.print(", ");
	}
	System.out.println();
}

The output should be similar to:

OSGi System Bundle, asm.osgi, log4j.osgi, spring-test, spring-osgi-test, spring-osgi-core, 
    spring-aop, spring-osgi-io, slf4j-api, 
spring-osgi-extender, etc... TestBundle-testOsgiPlatformStarts-com.your.package.SimpleOsgiTest, 

As you can see, the testing framework installs the mandatory requirements required for running the test such as the Spring, Spring-DM, slf4j jars among others.

7.2.2. Installing test prerequisites

Besides the Spring-DM jars and the test itself is highly likely that you depend on several libraries or your own code for the integration test.

Consider the following test that relies on Apache Commons Lang:

import org.apache.commons.lang.time.DateFormatUtils;
    ...
  	public void testCommonsLangDateFormat() throws Exception {
		System.out.println(DateFormatUtils.format(new Date(), "HH:mm:ssZZ"));
	}
}

Running the test however yields an exception:

java.lang.IllegalStateException: Unable to dynamically start generated unit test bundle
     ...
Caused by: org.osgi.framework.BundleException: The bundle could not be resolved. 
Reason: Missing Constraint: Import-Package: org.apache.commons.lang.time; version="0.0.0"
    ...
	... 15 more
	

The test requires org.apache.commons.lang.time package but there is no bundle that exports it. Let's fix this by installing a commons-lang bundle.

One can specify the bundles that she wants to be installed using getTestBundlesNames or getTestBundles method. The first one returns an array of String that indicate the bundle name, package and versioning through as a String while the latter returns an array of Resources that can be used directly for installing the bundles. That is, use getTestBundlesNames when you rely on somebody else to locate (the most common case) the bundles and getTestBundles when you want to locate the bundles yourself.

By default, the test suite uses the local maven2 repository to locate the artifacts. The locator expects the bundle String to be a comma separated values containing the artifact group, name, version and (optionally) type. It's likely that in the future, various other locators will be available. One can plug in their own locator through the org.springframework.osgi.test.provisioning.ArtifactLocator interface.

Let's fix our integration test by installing the required bundle (and some extra osgi libraries):

protected String[] getTestBundlesNames() {
	 return new String[] { "org.springframework.osgi, cglib-nodep.osgi, 2.1.3-SNAPSHOT",
	 	"org.springframework.osgi, jta.osgi, 1.1-SNAPSHOT",
	 	"org.springframework.osgi, commons-lang.osgi, 2.3-SNAPSHOT" };
	 };
}

Rerunning the test should show that these bundles are now installed in the OSGi platform.

Note

The artifacts mentioned above have to exist in your local maven repository.

7.2.3. Advanced testing framework topics

The testing framework allows a lot of customization to be made. This chapter details some of the existing hooks that you might want to know about. However, these are advanced topics as they increase the complexity of your test infrastructure.

7.2.3.1. Customizing test manifest

There are cases where the auto-generated test manifest does not suite the needs of the test. For example the manifest requires some different headers or a certain package needs to be an optional import. To provide your own manifest, simply override getManifestLocations():

protected String getManifestLocation() {
	return "classpath:com/xyz/abc/test/MyTestTest.MF";
}}

However, the manifest requires some the folowing entry:

Bundle-Activator: org.springframework.osgi.test.JUnitTestActivator

since without it, the testing infrastructure cannot function properly. Also, one needs to import JUnit, Spring and Spring-DM specific packages used by the base test suite:

Import-Package: junit.framework,
  org.osgi.framework,
  org.apache.commons.logging,
  org.springframework.util,
  org.springframework.osgi.service,
  org.springframework.osgi.util,
  org.springframework.osgi.test,
  org.springframework.context

Failing to import a package used by the test class will cause the test to fail with a NoDefClassFoundError error.

7.2.3.2. Customizing test bundle content

By default, for the on-the-fly bundle, the testing infrastructure uses all the classes, xml and properties files found under ./target/test-classes folder. This matches the project layout for maven which is used (at the moment by Spring-DM). These settings can be configured in two ways:

  1. programmatically by overriding AbstractConfigurableBundleCreatorTests getXXX methods.

  2. declaratively by creating a properties file having a similar name with the test case. For example, test com.xyz.MyTest will have the properties file named com/xyz/MyTest-bundle.properties. If found, the following properties will be read from the file:

    Table 7.1. Default test jar content settings

    Property NameDefault ValueDescription
    root.dirfile:./target/test-classesthe root folder considered as the jar root
    include.patterns/**/*.class, /**/*.xml, /**/*.propertiesComma-separated string of Ant-style patterns
    manifest(empty)manifest location given as a String. By default it's empty meaning the manifest will be created by the test framework rather then being supplied by the user.

    This option is handy when creating specific tests that need to include certain resources (such as localization files or images).

Please consult AbstractConfigurableBundleCreatorTests and AbstractOnTheFlyBundleCreatorTests tests for more customization hooks.

7.2.4. Creating an OSGi application context

Spring-DM testing suite builds on top of Spring testing classes. To create an application context (OSGi specific), one should just override getConfigLocations[] method and indicate the location of the application context configuration. At runtime, an OSGi application context will be created and cached for the lifetime of the test case.

protected String[] getConfigLocations() {
   return new String[] { "/com/xyz/abc/test/MyTestContext.xml" };
}

7.2.5. Specifying the OSGi platform to use

The testing framework supports out of the box, three OSGi 4.0 implementations namely: Equinox, Knopflerfish and Felix. To be used, these should be in the test classpath. By default, the testing framework will try to use Equinox platform. This can be configured in several ways:

  1. programmatically through getPlatformName() method

    .

    Override the aforementioned method and indicate the fully qualified name of the Platform interface implementation. Users can use the Platforms class to specify one of the supported platforms:

    protected String getPlatformName() {
       return Platforms.FELIX;
    }
  2. declaratively through org.springframework.osgi.test.framework system property.

    If this property is set, the testing framework will use its value as a fully qualified name of a Platform implementation. It that fails, it will fall back to Equinox after logging a warning message. This option is useful for building tools (such as ant or maven) since it indicates a certain target environment without changing and test code.

7.2.6. Waiting for the test dependencies

A built-in feature of the testing framework is the ability to wait until all dependencies are deployed before starting the test execution. Since the OSGi platforms are concurrent by nature, installing a bundle doesn't mean that all its services are running. By running a test before its dependency services are fully initialized can cause sporadic errors that pollute the test results. By default, the testing framework inspects all bundles installed by the user and, if they are Spring-powered bundles, waits until they are fully started (that is their application context is published as an OSGi service). This behaviour can be disabled by overriding shouldWaitForSpringBundlesContextCreation method. Consult AbstractSynchronizedOsgiTests for more details.

7.2.7. Testing framework performance

Considering all the functionality offered by the testing framework, one might wonder if this doesn't become a performance bottleneck. First, it's worth noting that all the work done automatically by the testing infrastructure has to be done anyway (such as creating the manifest or creating a bundle for the test or installing the bundles). Doing it manually simply simply does not work as it's too error prone and time consuming. In fact, the current infrastructure started as way to do efficient, automatic testing without worrying about deployment problems and redundancy.

As for the numbers, the current infrastructure has been used internally for the last half a year - our integration tests (around 120) run in about 3:30 on a laptop. Most of this time is spent on starting and stopping the OSGi platform: the "testing framework" takes around 10% (as shown in our profiling so far). For example, the manifest generation has proved to take less then 0.5 seconds in general, while the jar creation around 1 second.

However, we are working on making it even faster and smarter so that less configuration options are needed and the contextual information available in your tests is used as much as possible. If you have any ideas or suggestion, feel free to use our issue tracker or/and mailing list.

Hopefully this chapter showed how Spring-DM testing infrastructure can simplify OSGi integration testing and how it can be customized. Consider consulting the javadocs for more information.