| Testing OSGi-based Applications with DA-Testing Framework |
|
|
| Written by Valery Abu-Eid | |
| Tuesday, 31 March 2009 22:07 | |
|
If you use OSGi in order to make your application dynamic, then your tests should test the dynamicity aspect of your application, otherwise how would you know whether your application behaves dynamically or not? how would you be sure that clients use the service with highest rank when such is registered? that bundle updates wouldn't break the application? or that any other risks you have when working on dynamic OSGi-based applications are tested and verified properly? Surely, you wouldn't want to verify this behavior in production systems or manually. Agreeing on the importance of dynamicity tests, we will move to the next issue that developers of dynamic OSGi-based applications have, how to test dynamicity? Current Approach for Testing Dynamic Applications (xUnit based)Currently, all of the approaches, be they provided by OSGi vendors, like test support classes that ship with Spring-DM or any other custom efforts, revolve around the idea of running an OSGi Application inside JUnit tests then asserting different parts of the OSGi-based application using the Bundle Context. Below is an example test which verifies that a service which implements the AccountsService interface is registered after installing a bundle.
@Test
public void accountServiceVerifactionTest() throws Exception {
/// We assume that you've started the OSGi Environment either by
/// running it on your own or using helper or super classes.
BundleContext bundleContext = getBundleContext();
/// Here you use some code you developed to help you locate
/// the bundles you are testing
URL accountingBundleUrl = locateBundleBySymbolicName(
"org.test.accounting", "1.0.0");
bundleContext.installBundle(accountingBundleUrl.toString()).start();
ServiceReference accountServiceRef = bundleContext.getServiceReference(
"org.test.accounting.AccountService");
assertNotNull(accountServiceRef, "AccountService was not registered");
/// You can't execute the code below since it will throw a Class Casting
/// exception. This is due the fact that the OSGi Enviornment uses a
/// different Class Loader from the one JUnit uses
AccountService accountService = bundleContext.getService(accountServiceRef);
}
The approach above has few critical issues that can turn OSGi-based application testing experience into nightmare:
DA-Testing with a Dynamic Oriented and OSGi-friendly approachDA-Testing, the first testing framework intended for testing OSGi-based applications, moves away from the xUnit approach to a dynamic oriented one for testing dynamic OSGi-based applications. Why? Well, because we don't test units, be they classes or methods, but the dynamic behavior of the OSGi-based application. We apply runtime changes to the application and check how different components react to them. We don't ask questions like: What the result I will have if I pass these parameters? We ask questions like: If I remove this bundle or service, will the dependent components still perform normally? If I install a newer version of this bundle, will the behavior of the application change? or will it stay the same? Dynamicity is all about changes and reacting to them, as such, the testing approach we use should provide us with means that simplify the task of testing these changes. DA-Testing helps developers testing dynamic OSGi-based applications by implementing the concepts below:
Instead of explaining the concepts as a set of abstractions, we will tackle a task of testing a not very simple example application with DA-Testing. Testing Dynamic Store Application with DA-TestingThe Dynamic Store is a simple application that provides four services:
The application is designed to be flexible so it could be updated and patched easily, as such, we have the following bundles:
Each of the bundles above plus the OSGi Tests bundle has its Maven project. Please, note that the current release of DA-Testing is runnable only with Maven. Also, I removed all the comments and description text from the demonstrated code, if you want to read them, please refer to the source code. So, what we want from our tests?
Testing Data Storage service availability changesDefining requirements for dynamicity of our application, we always think of Dynamicity Scenarios. Defining dynamicity requirements we always say something like: What if service A became unavailable for a moment, how should dependent service B behave? If we provide a patch of bundle A which if its components need to be updated? etc. We define our dynamicity requirements this way, and so should be our tests, that's why the first thing we do when testing applications with DA-Testing is creating Test Scenarios (Dynamicity Scenarios). Now, lets see how the Test Scenario that emulates Data Storage service availability changes would look like:
@OsgiTestScenario(name = "Data Storage Availability Test Scenario",
description = "...",
testGroups = {
NormalServicesBehaviorTests.class,
DataStorageAvailabilityTests.class })
public class DataStorageAvailabilityTestScenario extends AbstractOsgiTestScenario {
@Override
public void prepare() throws Exception {
installAndStartBundles(getBundleStore().findBySymbolicNames(
SymbolicNames.DYNAMIC_STORE_API,
SymbolicNames.DYNAMIC_STORE_CORE));
}
@Override
public void perform() throws Exception {
registerService(DataStorage.class, new DataStorageServiceMock());
}
@Override
public void finish() throws Exception {
}
}
A Test Scenario in DA-Testing has three parts: prepare, perform and finish. Before performing a scenario, we prepare the OSGi Environment, and that's exactly what the prepare method does, we installed the API and Core bundles and started them, as you can see we installed these bundles in a single line of code (broken into 3 lines for the sake of clarity) without having to locate the bundle archives, also, DA-Testing will generate bundles from the OSGi-incompliant bundles if such exist (you can imagine how much time only these two features will save you). Any code executed in the prepare and finish methods is transparent to our tests, as such they will never be invoked based on changes done in these methods. So far so good, we have an OSGi Environment with two bundles, the API and the Core, but without a Data Storage service, while performing the scenario we register a mock Data Storage service. As you see the code is quite clean, the scenario code, the one which emulates the changes we want to see how our application reacts to, is completely separated from test assertions, as such, we can clearly express our dynamicity scenario in code. Now, we want to check how the application behaves when the Data Storage service is available and when it's not. You probably noticed that the @OsgiTestScenario annotation has the testGroups attribute, this attribute is responsible for specifying the classes that contain the tests which are responsible for testing application reaction to changes resulted from performing the Test Scenario. The DataStorageAvailabilityTests class has tests that check how the Order service reacts to Data Storage availability changes. Prior to demonstrating the full code of the DataStorageAvailabilityTests class, lets examine a single test which verifies that the placeOrder method accepts requests even when the Data Storage is unavailable:
@OsgiTest(name = "Place Order Async Processing Test",
description = "Place Order method of the OrderService must accept" +
" requests even when the DataStorage service is not available.")
@Conditions(serviceStateConditions = {
@ServiceStateCondition(serviceClass = ClassNames.ORDER_SERVICE,
state = ServiceState.AVAILABLE),
@ServiceStateCondition(serviceClass = ClassNames.DATA_STORAGE,
state = ServiceState.UNAVAILABLE)
})
public void testPlaceOrderAsyncProcessing() {
getService(OrdersService.class).placeOrder(new Order(999, 999));
}
As I mentioned before, tests are not executed randomly or sequentially, but in response to changes and the state of the OSGi Environment, as such, each test in DA-Testing has a condition annotation which specifies under which circumstances the test needs to be executed (you can also provide tests with no condition annotation, then it will be executed as soon as the Test Scenario performance begins). For instance, the test above is executed when the Order service is available and the Data Storage service is not, this happens in our Test Scenario prior to registering the mock Data Storage service. As you can see we acquired a reference to the Orders service and invoked the placeOrder method in a single line of code, another example of how the API is meant to simplify your life. Now, lets take a look at the full code of DataStorageAvailabilityTests class:
@OsgiTestGroup(name = "DataStorage Availability Tests")
public class DataStorageAvailabilityTests extends AbstractOsgiTestGroup {
@OsgiTest(name = "Place Order Async Processing Test", description = "...")
@Conditions(serviceStateConditions = {
@ServiceStateCondition(serviceClass = ClassNames.ORDER_SERVICE,
state = ServiceState.AVAILABLE),
@ServiceStateCondition(serviceClass = ClassNames.DATA_STORAGE,
state = ServiceState.UNAVAILABLE)
})
public void testPlaceOrderAsyncProcessing() {
getService(OrdersService.class).placeOrder(new Order(999, 999));
}
@OsgiTest(name = "Find Client Orders method unavailability Test",
description = "...")
@Conditions(serviceStateConditions = {
@ServiceStateCondition(serviceClass = ClassNames.ORDER_SERVICE,
state = ServiceState.AVAILABLE),
@ServiceStateCondition(serviceClass = ClassNames.DATA_STORAGE,
state = ServiceState.UNAVAILABLE)
})
public void testGetOrdersUnavailability() {
/// Although DA-Testing provides @ExpectedException annotation, currently,
/// it's usable only with general exceptions (not application specific).
/// This is due the fact that application specific exception classes are
/// available only after executing test scenarios, which causes class
/// loading conflicts when generating tests. Using the exception in
/// catch block will cause the same problem.
try {
getService(OrdersService.class).findClientOrders(1);
throw new ExpectedExceptionNotThrownException(
ServiceUnavailableException.class);
} catch (RuntimeException ex) {
if (!(ex instanceof ServiceUnavailableException)) {
throw new ExpectedExceptionNotThrownException(
ServiceUnavailableException.class);
}
}
}
@OsgiTest(name = "Orders data synchronization Test", description = "...")
@Conditions(serviceStateConditions = {
@ServiceStateCondition(serviceClass = ClassNames.ORDER_SERVICE,
state = ServiceState.AVAILABLE)
}, serviceEventConditions = {
@ServiceEventCondition(serviceClass = ClassNames.DATA_STORAGE,
event = ServiceEventType.REGISTERED)
})
public void testOrderDataSynchronization() {
/// We will give the OrderService some time to fill the
/// DataStorage service that was registered
sleep(50, MILLISECONDS);
/// We check that the order we created before DataStorage was
/// available is moved to the storage
getAssertions().assertTrue(
containsOrder(getService(DataStorage.class), 999, 999));
}
protected boolean containsOrder(
DataStorage dataStorage, int clientId, int productId) {
for (Order order : dataStorage.getOrders()) {
if (order.getClientId() == clientId
&& order.getProductId() == productId) {
return true;
}
}
return false;
}
}
DataStorageAvailabilityTestScenario uses two Test Groups, I didn't cover the second since it performs simple tests that verify the behavior of the application in normal circumstances and repeats much of what was presented above. Testing the Patch BundleThe test of the Patch Bundle should confirm that the application changes its behavior when we install the Patch Bundle and is capable of behaving as it used to do if we decide to roll back the patch. For that we created one Test Scenario PatchInstallationTestScenario which will install the patch then uninstall it, the assertions are in the Tests Group PatchInstallationTests. Below is the example code:
@OsgiTestScenario(name = "Patch Installation Test Scenario",
description = "...",
testGroups = {
NormalServicesBehaviorTests.class,
PatchInstallationTests.class })
public class PatchInstallationTestScenario extends AbstractOsgiTestScenario {
@Override
public void prepare() throws Exception {
installAndStartBundles(getBundleStore().findBySymbolicNames(
SymbolicNames.DYNAMIC_STORE_API,
SymbolicNames.DYNAMIC_STORE_CORE));
registerService(DataStorage.class, new DataStorageServiceMock());
}
@Override
public void perform() throws Exception {
/// We install the patch
Bundle patchBundle = installAndStartBundle(
getBundleStore().findBySymbolicName(
SymbolicNames.DYNAMIC_STORE_CORE_PATCH));
// Then remove it
patchBundle.uninstall();
}
@Override
public void finish() throws Exception {
}
}
@OsgiTestGroup(name = "Patch Installation Tests")
public class PatchInstallationTests extends AbstractOsgiTestGroup {
@OsgiTest(name = "Home Page HTML Verification Test", description = "...")
@ServiceStateCondition(serviceClass = ClassNames.WEB_RESOURCES_SERVICE,
state = ServiceState.AVAILABLE)
public void testHomePageHtml() {
getAssertions().assertEquals("Stay away, lousy clients!",
getService(WebResourcesService.class).getHomePageHtml());
}
@OsgiTest(name = "New Welcoming Service Test", description = "...")
@Conditions(
/// Of course we can skip on the Bundle State condition, but
/// I put it here for demo purposes
bundleStateConditions = {
@BundleStateCondition(
symbolicName = SymbolicNames.DYNAMIC_STORE_CORE_PATCH,
state = BundleState.STARTING)
},
serviceEventConditions = {
@ServiceEventCondition(
serviceClass = ClassNames.WELCOMING_SERVICE,
serviceFilter = "(updateReason=patch)",
event = ServiceEventType.REGISTERED)
})
public void testNewWelcomingService() {
getAssertions().assertEquals("Welcome, our lovely clients!",
getService(WelcomingService.class).welcomeClients());
}
@OsgiTest(name = "Home Page after New Welcoming Service Availability Test",
description = "...")
@ServiceStateCondition(serviceClass = ClassNames.WELCOMING_SERVICE,
serviceFilter = "(updateReason=patch)",
state = ServiceState.AVAILABLE)
public void testHomePageHtmlAfterNewWelcomingServiceAvailability() {
/// We give the WebResourcesService a moment to use the new
/// version of the Welcoming Service
sleep(50, MILLISECONDS);
getAssertions().assertEquals("Welcome, our lovely clients!",
getService(WebResourcesService.class).getHomePageHtml());
}
@OsgiTest(name = "Home Page after New Welcoming Service removal Test",
description = "...")
@ServiceEventCondition(serviceClass = ClassNames.WELCOMING_SERVICE,
serviceFilter = "(updateReason=patch)",
event = ServiceEventType.UNREGISTERING)
public void testHomePageHtmlAfterNewWelcomingServiceRemoval() {
/// We give the WebResourcesService a moment to move back to
/// the old version of the Welcoming Service
sleep(50, MILLISECONDS);
getAssertions().assertEquals("Stay away, lousy clients!",
getService(WebResourcesService.class).getHomePageHtml());
}
}
As you can see, other than the clear separation between dynamicity scenario and test assertions, the Bundle location logic, accessing OSGi objects, services invocation, checking conditions of the OSGi Environment, bundle generation, listening to events, etc. is all transparent/simplified for you. If you are not sure of the benefits you would be getting from using DA-Testing, you can test the same application with the current tool/approach you are using and compare code size and development time to achieve the same result - After all, the example dynamic application is already available and what is left is testing it. The source code of the example application is available here: dynamic-store-tests.zip. The 'dynamic-store-osgi-tests' Maven project can be used as a template for running your tests. If you decide to read the source code, please make sure to check the following resources:/dynamic-store-osgi-tests/pom.xml /dynamic-store-osgi-tests/src/main/resources/META-INF/MANIFEST.MF /dynamic-store-osgi-tests/src/main/resources/OSGI-TEST-INF/bundle-archives.xml
To execute the OSGi Tests, execute the command below from the root folder: Future WorkWe plan to make DA-Testing into the final release (1.0.0) within two to three months, hopefully, by then we will have three releases or more. Most of the work will be focused on:
|