This artifact provides support classes for OSGi testing with JUnit 5 including JUnit 5 Extensions.
There are a number of operations that can be performed with or on the OSGi BundleContext
. However, this is a
low level API that risks exposing other tests to side effects resulting from these operations. Managing these side effects can results in large amounts of boiler plate code.
The BundleContextExtension
is designed to help in these scenarios by giving access to an instance of the BundleContext that is context aware and results in the necessary cleanup when a test scope
ends. The following cleanup is performed at the end of each test scope:
- bundles installed with
installBundle
are uninstalled - services obtained from
getService
are returned ServiceObjects
instances obtained fromgetServiceObjects
are returned- services registered by
registerService
are unregistered BundleListener
s registered withaddBundleListener
are removedFrameworkListener
s registered withaddFrameworkListener
are removedServiceListener
s registered withaddServiceListener
are removed (this includes closingServiceTracker
s created using theBundleContext
)
Scope is inherited - for example, any services that are registered at the class-level
scope (eg, in a @BeforeAll
callback) will be visible to all tests in the class as
well as to all tests in all nested test classes, and then cleaned up after all the class'
tests have run. On the other hand, changes made during the execution of an individual
test will only be visible for the duration of that individual test. Similarly, @Nested
-annotated
inner test classes will be able to see changes made in their enclosing class' @BeforeAll
and @BeforeEach
methods.
The BundleContextExtension
can be applied declaratively to a test class using the JUnit5 @ExtendWith
annotation.
@ExtendWith(BundleContextExtension.class)
public class MyTest {
// ...
}
See the JUnit5 documentation for more information on Declarative Extension Registration.
The BundleContextExtension
can be applied programmatically using a public field of the test class annotated with the JUnit5 @RegisterExtension
annotation.
@RegisterExtension
public BundleContextExtension bundleContextExtension = new BundleContextExtension();
See the JUnit5 documentation for more information on Programmatic Extension Registration.
Now that the extension is in place, a BundleContext
instance can be injected into a non-private,
non-final field annotated with @InjectBundleContext
:
@InjectBundleContext
BundleContext bundleContext;
or from a likewise-annotated test method or lifecycle method parameter:
@BeforeAll
public static void beforeAll(
@InjectBundleContext BundleContext classScopeBundleContext) {
// changes made here will persist until the afterAll phase completes
}
@BeforeEach
public void beforeEach(
@InjectBundleContext BundleContext testScopeBundleContext) {
// changes made here will persist until the afterEach phase completes
}
@Test
public void testWithBundleContext(
@InjectBundleContext BundleContext testScopeBundleContext) {
// changes made here will persist until the afterEach phase completes
}
In OSGi testing there are many scenarios that require installing pre-built bundles. The Bnd tool has support for easily building and embedding bundles within bundles. As a matter of convenience the BundleInstaller
utility was designed to simplify the task of finding and installing such embedded bundles.
The BundleInstaller
utility provides three convenience methods:
Bundle installBundle(String pathToEmbeddedJar)
Bundle installBundle(String pathToEmbeddedJar, boolean startBundle)
BundleContext getBundleContext()
...to simplify these use cases. The installBundle
methods use the findEntries
method from the Bundle API to locate embedded bundles.
An instance of this utility can be injected into a field, test or lifecycle method (much like the BundleContext
)
using the @InjectBundleInstaller
.
@InjectBundleInstaller
BundleInstaller bundleInstaller;
or
@Test
public void testWithBundleInstaller(
@InjectBundleInstaller BundleInstaller bundleInstaller) {
// ...
}
Testing OSGi services can prove to be tricky business involving a lot of state management.
The ServiceExtension
was designed to help alleviate this complexity by offering a mechanism to declare which service or set of services to use and how they should behave within the context of a test and then to clean up when the test ends.
The following criteria can be defined for any service or set of services:
- a service type for the service
- a cardinality declaring the minimum number of required services
- a filter expression to match available services
- a timeout within which a service or set of services must arrive before the test fails
The ServiceExtension
can be applied declaratively to a test class using the JUnit5 @ExtendWith
annotation.
@ExtendWith(ServiceExtension.class)
public class MyTest {
// ...
}
See the JUnit5 documentation for more information on Declarative Extension Registration.
The ServiceExtension
can be applied programmatically using a field of the test class annotated with the JUnit5 @RegisterExtension
annotation.
@RegisterExtension
public ServiceExtension serviceExtension = new ServiceExtension();
See the JUnit5 documentation for more information on Programmatic Extension Registration.
With the extension in place, service instances can be injected into a non-private, non-static field annotated with @InjectService
@InjectService
LogService logService;
or from a likewise annotated test method parameter
@Test
public void testWithLogService(
@InjectService LogService logService) {
// ...
}
If the type of the field/parameter is of type java.util.List<S>
then the value will be a list of services of type S
where S
must not be a generic type.
@InjectService
List<LogService> logServices;
or from a likewise annotated test method parameter
@Test
public void testWithLogServices(
@InjectService List<LogService> logServices) {
// ...
}
The list of services is provided in natural order of their service references. Tests are free to manipulate the list.
The type org.osgi.test.common.service.ServiceAware
provides several introspection methods related to tracking services.
If the type of the field/parameter is of type org.osgi.test.common.service.ServiceAware<S>
then the value will be an instance of that type where S
is the service type and S
must not be a generic type.
@InjectService
ServiceAware<LogService> lsServiceAware;
or from a likewise annotated test method parameter
@Test
public void testWithLogServices(
@InjectService ServiceAware<LogService> lsServiceAware) {
// ...
}
The instance is live and will reflect the state of tracked services and the underlying service tracker. This allows for cases having zero cardinality and/or to dynamically register services in the test itself and observe them with the ServiceAware
instance.
The cardinality can be specified to indicate a minimum number of expected services using the @InjectService.cardinality
property:
@InjectService(cardinality = 2)
// Gets the first service in ranking order (although it still waits for 2)
LogService logService;
@InjectService(cardinality = 2)
// The more likely usage
List<LogService> logService;
@Test
public void testWithLogServices(
@InjectService(cardinality = 2) List<LogService> logServices) {
// ...
}
@InjectService(cardinality = 2)
ServiceAware<LogService> lsServiceAware;
The default cardinality is 1
.
Setting the cardinality to 0
allows for the test to continue immediately without waiting. This usage is most interesting used in conjunction with ServiceAware
fields/parameters which allows for a live view of the underlying tracker essentially giving a managed service tracker to use in tests. It must be noted that field or parameter types other than ServiceAware
are very likely to be null
or empty since potentially no value was available to populate them.
@InjectBundleContext
BundleContext bundleContext;
@InjectService(cardinality = 0)
ServiceAware<LogService> lsServiceAware;
@Test
public void testWithLogServices() {
bundleContext.registerService(LogService.class, new MyLogService(), null);
assertFalse(lsServiceAware.isEmpty());
}
Services can be filtered by declaring a filter expression using the @InjectService.filter
property.
@InjectService(filter = "(service.vendor=Acme Inc.)")
LogService logService;
As a matter of convenience the @InjectService.filterArguments
property can be used to provide format arguments where the @InjectService.filter
property uses format syntax.
@InjectService(
filter = "(%s=%s)",
filterArguments = {
Constants.SERVICE_VENDOR,
MyConstants.COMPANY_NAME
}
)
List<LogService> logServices;
A timeout can be applied in order to constrain the length of time the extension will wait before declaring a test failure. The timeout can be specified using the @InjectService.timeout
property.
@InjectService(timeout = 1000)
LogService logService;
The timeout will be cut short and the test will proceed when all other constraints of the @InjectService
are satisfied. As such, if all constraints can be immediately satisfied no waiting will occur.
The default timeout is 200
milliseconds.