OSGi Component Testing
In my last blog post I talked about Getting Started with OSGi Declarative Services. In this blog post I want to show how to test OSGi service components.
Unit testing / “white-box testing”
The first approach for testing components is to write unit tests. In a plain Java project such tests are added in an additional source folder (typically named test). They can then be executed from the IDE and at build time, but left out in the resulting JAR. With Bndtools the same approach is used for unit or white-box-testing.
In Eclipse RCP development you typically create a test fragment for the bundle that should be tested. This way the tests can be executed automatically via Tycho Surefire, when running the build process via Maven.
Note:
In one of my previous blog posts I wrote about the wrong usage of fragments in various projects. Also about when fragments should be used and when they shouldn’t. As I got feedback that I did not mention testing with Tycho, I want to add that with this post. Using a fragment for unit testing is also a valid approach. Compared with the typical Java approach to have the test code located in the same project in a separate source folder, using a fragment is similar and gives the same opportunities. The classes are in the same classpath and therefore share the same visibility (e.g. access to package private or protected methods).
Execute the following steps to create a test fragment for unit testing the StringInverterImpl
of the Getting Started Tutorial:
- Create a new fragment project File -> New -> Other -> Plug-in Development -> Fragment Project
- Set the name to org.fipro.inverter.provider.tests It is important that the name ends with .tests so we can later use pom-less Tycho for building.
- Set the host plug-in to org.fipro.inverter.provider
- Open the MANIFEST.MF file and switch to the Dependencies section
- Add org.junit to the Required Plug-ins To avoid Require-Bundle vs. Import-Package discussions, IMHO in this case it is perfectly fine to be dependent on the producer bundle. And as it includes and re-exports org.hamcrest.core, using Require-Bundle avoids several issues with regards to split packages.
- Create a new package org.fipro.inverter.provider
- Create a new JUnit4 based test class
package org.fipro.inverter.provider;
import static org.junit.Assert.assertEquals;
import org.fipro.inverter.StringInverter;
import org.junit.Test;
public class StringInverterImplTest {
@Test
public void shouldInvertText() {
StringInverter inverter = new StringInverterImpl();
assertEquals("nospmiS", inverter.invert("Simpson"));
}
}
The test can be executed via right click -> Run As -> JUnit Test
Integration testing / “black-box-testing”
Integration tests or black-box-tests are used to test if our bundle and the provided services behave correctly in an OSGi environment. This is especially necessary if the services to test reference other services or OSGi features are used, like the EventAdmin for event processing or the ConfigurationAdmin to configure components at runtime. Integration tests are contained in a test bundle, so also the bundle wiring is tested accordingly.
Execute the following steps to create a test bundle / plug-in for integration testing of the org.fipro.inverter.provider bundle from the Getting Started Tutorial:
- Create a new plug-in project File -> New -> Other -> Plug-in Development -> Plug-in Project
- Set the name to org.fipro.inverter.integration.tests It is important that the name ends with .tests so we can later use pom-less Tycho for building.
- Ensure that no Activator is generated, no UI contributions will be added and that no Rich Client Application is created.
- Open the MANIFEST.MF file and switch to the Dependencies section
- Add org.junit to the Required Plug-ins
- Add the following entries to the Imported Packages
- org.fipro.inverter
- org.osgi.framework
- org.osgi.util.tracker
- Create a new package org.fipro.inverter.integration.tests
Note:
We could also add org.fipro.inverter.provider to the Require-Bundle section, to make the integration test explicitly dependent on that provider bundle. And surely there are cases where this makes sense. In that case I would suggest to name the test bundle org.fipro.inverter.provider.integration.test to make that clear. But the explained approach in this tutorial simulates a real usage example of the service in other bundles, so IMHO that is a real integration test.
- Create a new JUnit4 based test class
package org.fipro.inverter.integration.tests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.fipro.inverter.StringInverter;
import org.junit.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.util.tracker.ServiceTracker;
public class IntegrationTest {
@Test
public void shouldInvertWithService() {
StringInverter inverter = getService(StringInverter.class);
assertNotNull("No StringInverter service found", inverter);
assertEquals("nospmiS", inverter.invert("Simpson"));
}
static <T> T getService(Class<T> clazz) {
Bundle bundle = FrameworkUtil.getBundle(IntegrationTest.class);
if (bundle != null) {
ServiceTracker<T, T> st =
new ServiceTracker<T, T>( bundle.getBundleContext(), clazz, null);
st.open();
try {
// give the runtime some time to startup
return st.waitForService(500);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
}
Note:
As the test is executed in the JUnit test runtime, we can not make use of the service binding mechanisms. We therefore need to find and access the service in a programmatical way. The above example is showing the proposed way of using a ServiceTracker
.
- Open the MANIFEST.MF file and switch to the Overview section
- Check Activate this plug-in when one of its classes is loaded which generates the
Bundle-ActivationPolicy: lazy
header in the MANIFEST.MF file. This is necessary so the test bundle is started, which is needed to have aBundleContext
for programmatical service loading.
- Check Activate this plug-in when one of its classes is loaded which generates the
The test can be executed via right click -> Run As -> JUnit Plug-in Test
Dealing with implicit dependencies
When executing the integration tests in the IDE, a launch configuration will be created, that automatically adds all bundles from the workspace and the target platform to the test runtime. This way all necessary bundles are available, even the implicit dependencies.
When executing a test bundle/fragment via Tycho Surefire, the OSGi runtime for the test execution consists of the test bundle/fragment and its dependencies. There is no explicit launch configuration. Because of that, the implicit dependencies need to be specified in another way to add them to the test runtime. In general you need to make the implicit dependencies explicit. This can be done in different ways. The most obvious is to add a bundle requirement to the test bundle dependencies. But as explained above, this is more a workaround than a solution. The suggested way in various wiki entries and blog posts is to configure the additional dependencies for the test runtime in the pom.xml. More information on that can be found in the following documentations and blogs:
- Tycho Packaging Types - eclipse-test-plugin
- Tycho FAQ - How to test OSGi declarative services?
- Tycho Test Trouble - Expectations and Realities
- Testing with Surefire
- Tycho Surefire Plugin
With the rise of pom-less Tycho builds the usage of explicit pom.xml files for test bundles and test fragments is not needed and wanted anymore. It is of course still possible to specify an explicit pom.xml to add special configurations. But if it is not necessary, it should be avoided to let the pom-less extension derive the necessary build information.
In the given example the integration test bundle has two implicit dependencies:
org.eclipse.equinox.ds
The Service Component Runtime that is needed to manage components and their life cycle.org.fipro.inverter.provider
The actual service provider bundle. As we only specified the package dependency on the service interface, there is no direct dependency on the provider.
To specify such implicit dependencies as explicit, OSGi capabilities can be used. Since Eclipse Neon org.eclipse.equinox.ds
provides the osgi.extender capability for osgi.component. And in the Getting Started Tutorial we added the osgi.service capability for the StringInverter
service interface. The corresponding Require-Capability header that needs to be added to the MANIFEST.MF file looks like the following snippet:
Require-Capability: osgi.extender;
filter:="(&(osgi.extender=osgi.component)(version>=1.2)(!(version>=2.0)))",
osgi.service;
filter:="(objectClass=org.fipro.inverter.StringInverter)"
Tycho is using the p2 resolver for resolving dependencies. But the p2 resolver does not support OSGi capabilities. In order to make the resolving work, the p2 capabilities need to be used. This can be done by adding a p2.inf file in the META-INF folder next to the MANIFEST.MF. The content of this file should look similar to the following snippet:
requires.0.namespace = osgi.extender
requires.0.name = osgi.component
requires.0.version = 1.2.0
requires.1.namespace = osgi.service
requires.1.name = org.fipro.inverter.StringInverter
At last we setup a pom-less Tycho build to proof that everything is working as expected. This means to simply create a .mvn/extensions.xml descriptor file in the root of the project directory, and a parent pom.xml file to configure the build. For further information on setting up a build with Tycho, have a look at the vogella Tycho Tutorial or the Tycho Wiki.
After the two files are in place and configured correctly, the build can be startet via
mvn clean verify
If everything is setup correctly, the build should run the unit test fragment and the integration test bundle, and the build should succeed.
The sources of the Getting Started Tutorial are located on GitHub and are updated for the contents of this Component Testing Tutorial.
If you are interested in testing OSGi bundles with Bndtools, you can have a look at the enRoute documentation.
Update:
After playing around some more I realized that it is not necessary to specify the capability requirement to the osgi.extender
in the integration test bundle. Neither via OSGi capabilities nor via p2.inf. The reason for this is that the capability requirement is already specified in the bundle that provides the org.fipro.inverter.StringInverter
service. By specifying the capability requirement to that service via Require-Capability
and p2 meta-data, the osgi.extender
capability is added transitively.