Ouertani Slim was born in Tunisia in 1981. Now he is a software engineer since 2004 and he is Java 6 and Spring certified with 100% and 94% respectively. He is very interested in Java technology, Scala and open source projects. He believes that programming is one of the hardest jobs and most beautiful in the world. Slim has posted 32 posts at DZone. You can read more from them at their website. View Full User Profile

OSGi : Junit Test Extender Using Fragment and BundleTracker

10.02.2009
| 10850 views |
  • submit to reddit

I-Introduction :

As Service tracker, the Osgi 4.2 release introduced "BundleTracker" which simplifies working with bundles and facilitate building extenders. In this article, we will try to write a simple JUnit test extender using fragments (to separate tests from source code). We will give to the user opportunities to automate the unit test just after or when the fragment bundle is installed, plus a console command to test all installed fragments or a specific one.

II- BundleTracker

We will use BundleTrackerCustomizer to track only Resolved Bundle state as fragments will not be at started state and to be sure that this fragment has a resolved parent. 
bundleTracker = new BundleTracker(context, Bundle.RESOLVED, testExtender);
bundleTracker.open();

where TestExtender
1 - implements BundleTrackerCustomizer
2- check if it is a test fragment
        private static final String TEST_HEADER = "Unit-Test";
public static final boolean isTestFragment(Bundle bundle) {
String header = bundle.getHeaders().get(TEST_HEADER) + "";
String fragment = bundle.getHeaders().get(org.osgi.framework.Constants.FRAGMENT_HOST) + "";
return (!"null".equals(header) && !"null".equals(fragment));
}
3- If yes, add (or update) it to map using it's bundle id.
 private Map bundles = Collections.synchronizedMap(new HashMap());
bundles.put(bundle.getBundleId(), bundle);

4- check if auto test is enabled :
 public static final boolean isAutoTestEnabled(Bundle bundle) {
return "true".equals(bundle.getHeaders().get(TEST_HEADER) + "");
}
if (isAutoTestEnabled(bundle)) {
test(bundle.getBundleId());
}

III- Test

Let's define loadClass method to load class from bundle and getHostBundle to get the parent Bundle
public static final Class loadClass(String clazz, Bundle bundleHost) {
try {
Class loadClass = bundleHost.loadClass(clazz);
return loadClass;

} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
 public static final Bundle getHostBundle(BundleContext context,Bundle bundle) {
String fragment = bundle.getHeaders().get(org.osgi.framework.Constants.FRAGMENT_HOST) + "";
Bundle[] bundles = context.getBundles();
for (Bundle ibundle : bundles) {
if (ibundle.getSymbolicName().equals(fragment)) {
return ibundle;
}
}
throw new RuntimeException();
}

loadClass and getHostBundle will be used to load all test class from fragment using hostBundle context!!!! (fragment will be loaded using it's host context loader) and try to load all class with Test suffix recursively.
 public static final List> getTestClass(BundleContext context,Bundle bundle) {
List> clazzs = new ArrayList>();
Enumeration entrs = bundle.findEntries("/", "*Test.class", true);
if (entrs == null || !entrs.hasMoreElements()) {
return Collections.EMPTY_LIST;
}
Bundle hostBundle = getHostBundle(context,bundle);
while (entrs.hasMoreElements()) {
URL e = (URL) entrs.nextElement();
String file = e.getFile();

String className = file.replaceAll("/", ".").replaceAll(".class", "").replaceFirst(".", "");
Class clazz = loadClass(className, hostBundle);
clazzs.add(clazz);
}
return clazzs;
}

for each loaded test Class we inspect it to find junit annotated methods :
public static final Test inspectClass(Class clazz) {
Test test = new Test();
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method method : declaredMethods) {

if (method.isAnnotationPresent(org.junit.BeforeClass.class)) {
test.setBeforeClass(method);
}
if (method.isAnnotationPresent(org.junit.AfterClass.class)) {
test.setAfterClass(method);
}
if (method.isAnnotationPresent(org.junit.Before.class)) {
test.setBefore(method);
}
if (method.isAnnotationPresent(org.junit.After.class)) {
test.setAfter(method);
}
if (method.isAnnotationPresent(org.junit.Test.class)) {
test.addTest(method);
}
}
return test;
}
and then, emulate junit mechanism using reflexion :
  public static void testClass(Test testClass, Object object) {
System.out.println("___________________________________________________________________________");


try {
try {
if (testClass.getBeforeClass() != null) {
testClass.getBeforeClass().invoke(object, new Object[0]);
}

List tests = testClass.getTests();
for (Method method : tests) {
try {
if (testClass.getBefore() != null) {
testClass.getBefore().invoke(object, new Object[0]);
}
try {
method.invoke(object, new Object[0]);
System.out.println("Method : [ "+ method.getName()+" ] PASS " );
} catch (Exception ex) {
System.out.println("Method : [ "+ method.getName()+" ] ERROR " );
}
} finally {
if (testClass.getAfter() != null) {
testClass.getAfter().invoke(object, new Object[0]);
}
}
}
} finally {
if (testClass.getAfterClass() != null) {
testClass.getAfterClass().invoke(object, new Object[0]);
}
}

} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("___________________________________________________________________________");
}

the object passed to this method is a new instance of class this is why don't forget ( DynamicImport-Package: *)
  for (Class clazz : testClazzs) {
try {
System.out.println("CLASS : ["+clazz.getName()+"]");
Test inspectClass = EClassUtils.inspectClass(clazz);
EClassUtils.testClass(inspectClass, clazz.newInstance());
} catch (Exception ex) {
ex.printStackTrace();
}

}
testAll loops all registered test.
public void testAll() {
Set> entrySet = bundles.entrySet();
System.out.println("===========================================================================");
for (Entry entry : entrySet) {
test(entry.getKey());
}
System.out.println("============================================================================");
}

IV- Console

to give the user opportunities to test specific fragment or do all test our activator register CommandProvider service with 3 methods starting (_) :
 public Object _test(CommandInterpreter intp) {
String nextArgument = intp.nextArgument();
testExtender.test(Long.parseLong(nextArgument));
return null;
}


public Object _testall(CommandInterpreter intp) {
testExtender.testAll();
return null;
}

public Object _helpTest(CommandInterpreter intp) {
String help = getHelp();
System.out.println(help);
return null;
}

(and _helpTest and not -help to conserve equinox help too)
@Override
public String getHelp() {
StringBuilder buffer = new StringBuilder();
buffer.append("---Testing commands---\n\t");
buffer.append("test [bundle id] - test bundle fragment id\n\t");
buffer.append("testall - test all fragments\n\t");
buffer.append("help - Print this help\n");
return buffer.toString();
}

V-Fragment

our fragment contains 2 class one is to be tested (OneTest):
import static org.junit.Assert.*;
public class OneTest {

@Test
public void echo() {
assertFalse(false);
}

@Test
public void fail() {
assertTrue(false);
}
}


with manifest headers :
Fragment-Host: com.jtunisie.osgi.client
Unit-Test: true

Output :

30      ACTIVE      com.jtunisie.osgi.test.extender_1.0.0.SNAPSHOT
31      ACTIVE      com.jtunisie.osgi.client_1.0.0.SNAPSHOT
                    Fragments=32
32      RESOLVED    com.jtunisie.osgi.fragment.test_1.0.0.SNAPSHOT
                    Master=31

osgi> test 32
Bundle : [32] : com.jtunisie.osgi.fragment.test
_
CLASS : [com.jtunisie.osgi.fragment.test.OneTest]
___________________________________________________________________________
Method : [ fail ] ERROR
Method : [ echo ] PASS
___________________________________________________________________________

conclusion :


Hope this is article help you to test your bundles using clean fragment. Project can be exported to 4.1 release using listeners but BundleTracker is very helpful .

source code is shared in kenai : http://kenai.com/projects/testosgifragment
Published at DZone with permission of its author, Slim Ouertani.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)