一种轻量级、可重用、可扩展的 OSGi 应用程序测试框架

引言

OSGi 是一个基于 Java 的,提供动态模块加载和管理的运行时框架,在业界已经得到广泛应用。OSGi 框架使用 Bundle 把复杂的应用程序模块化。在 OSGi 的框架中,Bundle 的生命周期由 OSGi 运行环境进行管理;Bundle 之间以松耦合的形式相互依赖;Bundle 有严格的访问安全限制。但也正是由于以上这些特点,给测试这些 Bundle 带来了很大的困难。许多测试用例要求被测 Bundle 及其依赖的 Bundle 同时运行于 OSGi 环境中;同时若需将测试代码和业务代码分离,则由于 Bundle 访问权限的限制,而无法进行细粒度的单元测试。 本文将介绍一种轻量级、可重用、可扩展的 OSGi 应用程序测试框架。该框架可以在 OSGi 环境中执行传统的 JUnit 测试代码。既能将测试用例和业务逻辑完全隔离,又不受 Bundle 之间的访问安全限制,从而可以在此框架上进行任何粒度的测试。另外这个框架还提供了丰富的用户界面 ( 比如 Telnet, 网页等 ) 以及测试结果报告,熟悉 JUnit 的用户可以没有任何障碍地使用这个框架来更好地测试他们的 OSGi 应用程序。

OSGi 是一个基于 Java 的,提供动态模块加载和管理的运行时框架。OSGi 为 Java 程序模块化和重用提供了可能。程序模块在 OSGi 运行框架中能够被动态加载、启动、停止和卸载,并且程序模块的生命周期由 OSGi 运行时框架进行管理。所有的程序模块可以相互发现和调用,这些组件可以组装在一起,在 OSGi 运行框架中相互协同运作。OSGi 作为一个行业标准,已经被广泛实现和应用于很多知名的产品。比如著名的开源 IDE Eclipse 就是基于 OSGi 标准并且提供了自己的实现。具体关于 OSGi 的详细介绍请参考资料中提供的链接。

OSGi 的架构为 Java 程序的模块化开发和模块的重用提供了很多便利,但是也正是由于 OSGi 中架构的一些限制,使得目前的 Java 测试方法(比如 JUnit)无法直接应用于 OSGi 运行时框架。

  • 在 OSGi 中使用 Bundle 来定义个程序模块,Bundle 包含了一组 Java 类以及一些附加的描述信息。Bundle 的生命周期由 OSGi 运行是框架维护,而且 Bundle 之间往往具有相互依赖和调用的关系。所以 OSGi 下的测试用例,需要运行在 OSGi 的运行框架中,并且往往需要多个 Bundle 之间的交互。而传统的 JUnit 测试方法,并没有提供测试用例在 OSGi 下的运行方法。
  • Bundle 对其包含的 Java 包有严格的访问限制,只有少数的接口包能够被其他的 Bundle 访问到。而在测试中,我们经常需要测试 Bundle 内非 Public 的包。如何避开 Bundle 的访问限制,测试 Bundle 内部包成为 OSGi 测试中的一个新问题。
  • 在编写测试代码是,我们总是希望做到测试代码能够和业务代码分离,也就是说在能够很好的运行测试用例的同时,产品发布时又不需要太多的工作来分离测试代码。在 OSGi 框架下,如何分离测试代码和业务代码,同时又不影响测试代码访问业务代码,也成为一个新的问题。

本文将针对以上提到的问题,介绍一种轻量级、可重用、可扩展的 OSGi 测试方案,并用具体的例子来介绍如何使用该方法。

基本原理

如图 1 所示,我们将测试代码(Test Suite)封装在单独的 Bundle 运行在 OSGi 中。并且提供了 Test Framework Bundle 来管理测试 Bundle。Test Framework Bundle 提供了用户接口,用户可以通过调用该接口来运行相应的测试用例,并且得到测试结果。


图 1. 测试框架基本原理
图 1. 测试框架基本原理  

framework_overview.bmp

  • Test Suite 中包含了所有的测试代码,这些代码可以是 JUnit 的测试用例或者是其他形式的测试代码。在 OSGi 启动阶段,每个 TestSuite 都会向 Test Framework Bundle 注册自己所包含的测试用例。当注册成功后,这些测试用例可以被 Test Framework Bundle 保存起来,并在之后的测试过程中被调用到。
  • Test Framework Bundle 是整个框架中的核心部分。它能够管理 Test Suite 所注册的测试用例。同时当用户需要执行某一测试用例时,他能找到该测试用例所对应的 Test Suite,然后执行 Test Suite 中的相应方法来运行改测试用例。最后,Test Framework Bundle 还能获得测试用例的执行结果并且输出给用户。

基于 OSGi/Eclipse 的测试框架可以有多种实现形式,在这里将介绍三种典型的实现方式,分别为基于 bundle/service 的实现,基于 fragment 的实现,以及基于 extension point + fragment 的实现。其中就框架的隔离性和便利性而言,第三种基于 extension point + fragment 的实现最为有效,但由于其需要使用 Eclipse 平台提供的 extension point 机制,其实用性受到一定的限制,只能用于 Eclipse 的 OSGi 实现 Equinox 中。

基于 bundle/service 的实现

利用服务层(service layer)提供的机制来实现模块间的松耦合,是 OSGi 平台的一大特性。在 OSGi 平台上实现 JUnit 测试框架,最直接的做法就是实现一个包含 JUnit 测试框架的 bundle,然后将相应的接口利用 service 的方式提供给 Test Bundle 的书写者。图 2 显示了这一实现的原理图。

这种方式的实现利用 OSGi 提供的松耦合机制将实际的业务逻辑代码和测试代码分离,同时又能充分利用 OSGi 的运行时环境,使得测试环境和真实运行环境一致。但是这种方式需要引入新的 bundle 来装载用户的测试用例,增加了系统的复杂度;同时使用 service 接口也有一些性能的损失。


图 2. 基于 bundle/service 的实现框图
图 2. 基于 bundle/service 的实现框图  

framework_impl_1.bmp

基于 fragment 的实现

在 OSGi 框架中提供了一种称为 fragment 的特殊 bundle。在装载的过程中,这类 fragment 是被附加到被称为“宿主”的 bundle 上,最后作为一个整体 bundle 运行于 OSGi 环境中。最为典型的 fragment 应用场景是多语言包发布,将包含多语言包的 bundle 作为主体程序 bundle 的 fragment,这样可以将多语言包和主体 bundle 作为两个独立的部分来发布,但在运行时它们又是一个整体。


图 3. 基于 fragment 的实现框图
图 3. 基于 fragment 的实现框图  

framework_impl_2.bmp

在实现测试框架时,也可以应用同样的思想。图 3 显示了这一实现的框图。测试框架只提供一个主测试 bundle,实现了 JUnit 的测试框架,但是它不需要注册相应的 service 给其他 bundle 使用,而只需提供测试代码的调用接口。而用户实现相应的测试用例时,需将其实现成主测试 bundle 的 fragment。在测试用例执行期间,真正的测试用例和主测试框架运行于一个整体的 bundle 内。这种机制相对于第一种实现方式,可以避免引入更多相对重量级的测试 bundle。同时也做到了将测试框架和测试用例分离,在运行时动态加载。可能存在的问题是当测试用例数量很大时,可能会造成测试 bundle 的运行时体积较大,但这种情况可以通过动态地卸载一些暂时不用的测试用例来解决。

基于 extension point + fragment 的实现

在第二种实现方法中,引入 fragment 机制是为了提供一种轻量级的分离测试框架和具体测试用例的方法。我们提出这个基于 OSGi 测试框架的一个目的是要分离测试用例和业务逻辑代码。那么能否也使用 fragment 的机制来达到这个目的呢?答案是肯定的,不仅仅提供测试框架的 bundle 可以作为“宿主”,被测 bundle 也可以作为“宿主”。图 4 显示了这一实现的框图。


图 4. 基于 extension point + fragment 的实现框图
图 4. 基于 extension point + fragment 的实现框图  

framework_impl_3.bmp

在这一实现中,我们将测试用例作为被测 bundle 的 fragment,这样在运行时,测试用例可以和被测代码运行于同一 bundle 中,免去了导出(export)一些被测包(package)的麻烦,可以完全做到不修改被测 bundle。发布正式产品时只需要去除那些包含测试用例的 fragment 即可。而 JUnit 测试框架运行于另一个独立的 bundle 中,为了让测试用例能够正确执行,可以采用第一种实现中所述的 service 方式,而另一种方式是利用 Eclipse 平台提供的 extension point 机制。测试框架 bundle 需要定义一些 extension point,而测试用例则需实现这些 extension point。这样就把测试用例和测试框架关联起来了。

这种实现的好处是结构清晰,测试用例也便于管理。但是由于要依赖 Eclipse 平台提供的 extension point 机制,其适用性受到一定的限制。同时如果测试用例本身就需要和被测 bundle 分离,那么这样的实现方式可能会无法满足某些测试目的。

在下面的章节中,我们将通过具体的实例介绍第三种“基于 extension point + fragment 的实现”的具体实施过程。

在 OSGi 中添加的 JUnit 测试框架

该框架主要包括两方面的功能:

  1. 为测试用例提供 JUnit 测试框架的支持
  2. 接受其他测试 Bundle 注册的测试用例,并且能够运行这些测试用例。

OSGi 本身并没有包含类似 JUnit 的测试框架。为了能够在 OSGi 中执行 JUnit 测试用例,我们需要创建一个包含了 JUnit 的 Bundle,同时把 JUnit 的功能 Export 出来,以便其他测试用例可以引用和调用。具体的步骤如下:

  1. 在 Eclipse 中,创建一个“Plug-in Project”,命名为 junit38_on_osgi,选择 Target Platform 为“an OSGi framework”,其余选项可以保持默认值。
  2. 在成功创建 Project 后,把 junit.jar 拷贝到工程目录根下,并且加入到 classpath 中。
  3. 修改 MANIFEST.MF 文件,把 junit.jar 的类 Export 出来,同时在 build.properties 中,把 junit.jar 打包在发布的 bundle 中。(具体的代码见清单 1 和清单 2,增加的代码以粗体显示)

最后 Eclipse 中的工程如图 5 所示。


图 5. OSGi 中的 JUnit 测试框架
图 5. OSGi 中的 JUnit 测试框架  

junit38_on_osgi_workspaces.bmp

接下来,我们将通过定义 Extension Point 的方式,来接受其他 Plug-in 注册测试用例。首先编辑 MANIFEST.MF 文件,定义一个新的 Extension Point。具体的定义如图 6 所示


图 6. 新建 Extension Point
图 6. 新建 Extension Point  

junit38_on_osgi_extension_point.bmp

Eclipse 会自动生成一个 XML 文件 schema/junit38_on_osgi.TestSuite.exsd。我们需要编辑该文件,添加具体的 Extension Point 的接口定义。具体的定义如图 7 所示


图 7. Extension Point 的接口定义
图 7. Extension Point 的接口定义  

junit38_on_osgi_extension_point_define.bmp

在这个接口定义中,我们引入了一个新的接口类“junit38_on_osgi.ITestSuite”。所有扩展这个 Extension Point 的 Plug-in,都通过实现这个接口,把它所包含的测试用例注册给测试框架。具体的接口定义文件见清单 3


清单 1. MANIFEST.MF
				
 Manifest-Version: 1.0 
 Bundle-ManifestVersion: 2 
 Bundle-Name: Junit38_on_osgi Plug-in 
 Bundle-SymbolicName: junit38_on_osgi 
 Bundle-Version: 1.0.0 
 Bundle-Activator: junit38_on_osgi.Activator 
 Bundle-RequiredExecutionEnvironment: J2SE-1.5 
 Import-Package: org.osgi.framework;version="1.3.0"
 Export-Package: junit.framework 


清单 2. build.properties
				
 source.. = src/ 
 output.. = bin/ 
 bin.includes = META-INF/,\ 
               .,\ 
               junit.jar
			


清单 3. ITestSuite.java
				
 package junit38_on_osgi; 
 import junit.framework.Test; 
 public interface ITestSuite { 
	 public Test getTests(); 
 } 

从工程“junit38_on_osgi”中导出 plug-in,然后加载到 OSGi 的运行时环境,我们就可以在 OSGi 中使用 JUnit Framework 中提供的类来编写和运行我们的测试用例。

编写与业务逻辑隔离的测试用例

在编写测试用例时,通常希望测试用例代码和业务代码能够相互隔离,以便在产品发布的时候,不会把这些不必要的测试代码也发布出去。在 OSGi 中,我们推荐把 JUnit 测试用例封装在单独的 plug-in 或者 fragment 中。下面我们将通过创建一个对 Eclipse 自带的 Hello World Plug-in 的测试,介绍测试用例的具体编写和执行方法。

创建被测试的 Hello Service Plug-in

首先我们使用 Eclipse 自带的一个 OSGi Hello Service 范例作为被测试代码。具体步骤如下:

  1. 在 New Project 中选择创建 Plug-in Project,
  2. 输入项目名为 helloworld,并且选择 Target Platform 为“an OSGi framework”
  3. 在 Templates 页面,选择从模板创建,选择模板为“Hello OSGi Service”
  4. 其余的选项都可以保持默认值,单击完成,创建工程

这个 Plugin 的功能是向 OSGi 注册一个名叫“HelloService”的服务,这个服务提供两个方法:speak 和 yell,分别输出“Howdy y'all”和“HOWDY Y'ALL!!!”。我们的测试代码将测试这两个方法。


图 8. HelloWorld 工程
图 8. HelloWorld 工程  
				helloworld_workspaces.bmp
			

编写测试用例

测试用例的编写,和传统的 JUnit 测试用例编写基本相同。唯一的区别是需要把这些测试用例封装在一个 Fragment 中,并且注册到测试框架(junit38_on_osgi)。

首先创建一个 Fragment Project,命名为 helloworldtest,选择这个 Fragment 的 Plug-in 为 helloworld,这样 Fragment 中的代码就可以完全访问被测试 plug-in(helloworld)的代码。工程创建完成后,我们需要编辑 MANIFEST.MF 文件,把测试框架(junit38_on_osgi)添加到依赖的 Plug-in 列表里面。如图 9 所示。


图 9. 添加依赖的 Plug-in
图 9. 添加依赖的 Plug-in  

dependent_plugin.bmp

然后我们与传统的 JUnit 测试用例编写一样,添加测试用例。这里,我们添加了对 HelloServiceImpl.java 的测试用例,如清单 4 所示。


清单 4. Helloworld 的测试用例代码
package helloworldtest;

import helloworld.HelloServiceImpl;
import junit.framework.TestCase;

public class HelloWorldTester extends TestCase {
public void testSpeak() {
HelloServiceImpl inst = new HelloServiceImpl();
inst.speak();
assertTrue(true);
}
}

我们需要把测试用例通过 Extension 的方式注册到测试框架(junit38_on_osgi)中。首先我们创建一个类,继承 ITestSuite,这个类能够返回一个 TestSuite 给测试框架。代码见清单 5


清单 5. Helloworld 的测试套件代码
package helloworld;

import junit.framework.Test;
import junit.framework.TestSuite;
import junit38_on_osgi.ITestSuite;

public class AllTests implements ITestSuite{

public static Test suite() {
TestSuite suite = new TestSuite("Test for helloworld");
//$JUnit-BEGIN$
suite.addTestSuite(HelloWorldTester.class);
//$JUnit-END$
return suite;
}

@Override
public Test getTests() {
return AllTests.suite();
}

}

在 Fragment 中定义 Extension,扩展 junit38_on_osgi.TestSuite,并且把“AllTest.java”传入这个 Extension。如图 10 所示


图 10. 扩展 Extension Point
图 10. 扩展 Extension Point  

implement_extension_point.bmp

这样,我们的测试代码就完成了。

运行测试用例

为了在 OSGi 运行环境中执行测试用例,我们需要在测试框架(junit38_on_osgi)中加入执行测试用例的代码。我们添加了一个类“TestRunner”来遍历和执行所有的测试用例。


清单 6. 执行测试用例代码
				
 package junit38_on_osgi; 

 import java.util.HashMap; 
 import java.util.Iterator; 
 import java.util.Map; 

 import junit.framework.Test; 
 import junit.framework.TestResult; 
 import org.eclipse.core.runtime.IConfigurationElement; 
 import org.eclipse.core.runtime.IExtension; 
 import org.eclipse.core.runtime.Platform; 

 public class TestRunner { 

  public void runAllTest() { 
    Map<String, Test> testcases = getTestCases(); 
    Iterator<String> iter = testcases.keySet().iterator(); 
    TestResult result = new TestResult(); 
    while (iter.hasNext()) { 
      Test test = testcases.get(iter.next()); 
      test.run(result); 
    } 
  } 

  private Map<String, Test> getTestCases() { 
    Map<String, Test> testCases = new HashMap<String, Test>(); 
    IExtension[] extensions = Platform.getExtensionRegistry() 
        .getExtensionPoint("junit38_on_osgi.junittestcase").getExtensions(); 
    for (IExtension extension : extensions) { 
      IConfigurationElement[] configElems = extension 
          .getConfigurationElements(); 
      for (IConfigurationElement configElem : configElems) { 
        
        try { 
          Object testCase = configElem.createExecutableExtension("class"); 
          String testCaseName = testCase.getClass().getName(); 
          if (testCases.containsKey(testCaseName)) { 
            System.out.println("Warnning: getTestCases - Duplicate test case "
                                + testCaseName); 
            continue; 
          } 
          if (testCase instanceof ITestSuite) { 
            testCases.put(testCaseName, ((ITestSuite) testCase).getTests()); 
          } 
        } catch (Exception e) { 
          System.out 
              .println("Error: getTestCases exception loading extension "
                  + extension.getUniqueIdentifier()); 
          e.printStackTrace(); 
        } 
      } 
    } 

    return testCases; 
  } 
 } 

然后在 junit38_on_osgi 的 Activator.java 中,调用 TestRunner 来执行所有的测试用例。


清单 7. 调用测试用例代码
				
 public void start(BundleContext context) throws Exception { 
	 super.start(context); 
	 plugin = this; 
	 TestRunner runner = new TestRunner(); 
	 runner.runAllTest(); 
 } 

在 Eclipse 中通过 Run OSGi Framwork 运行 junit38_on_osgi,运行“ss”可以看到 junit38_on_osgi, helloworld 作为两个 plug-in 运行在 OSGi 中,而 helloworldtester 作为 helloworld 的 fragment 运行在 OSGi 中。我们执行命令“start 8”(8 为 junit38_on_osgi 在 OSGi 中的 ID),可以看到测试用例被调用,并且输出“Howdy y ’ all”。

上面的例子中,我们只能在 Plug-in 启动的时候执行测试用例。我们可以提供更加丰富的接口,比如提供 HTTP 接口,用户可以通过浏览器来执行测试用例,获取执行结果。

为了使用 Web 界面来访问运行测试用例,用户需要在 test framework 这个 bundle 中实现一个 HttpServlet,并在这个 servlet 中处理相应的 http request,在 http request 中可以包含需要运行的测试用例的名字。在 servlet 接收到 http request 之后可以解析出需要运行的测试用例,并运行这个测试用例将运行结果通过 html 的方式返回给用户。通过这种方式,用户就可以很方便地通过浏览器来运行测试用例。

为了实现 HttpServlet,可以使用以下几个 bundle:org.eclipse.equinox.http,org.eclipse.equinox.servlet.api,org.eclipse.osgi.services。在 org.eclipse.equinox.servlet.api 中包含了一个类 javax.servlet.http.HttpServlet,用户需要继承这个类来实现自己的 Servlet。除此之外,用户需要通过 ServiceListener 来跟踪 org.osgi.service.http.HttpService 这个 OSGi Service,它在

org.eclipse.osgi.services 中实现。在 ServiceListener 中可以在 ServiceEvent.REGISTERED 这个事件时设置自己的 Servlet。

用户也可以通过 Telnet 调用 JUnit 的接口。这里可以使用一个名为 telnetD 的开源项目,此项目使用 Java 语言实现了一个 telnet 服务器,同时还提供很好的扩展接口来实现不同的 Terminal 形式。可以很方便地将这个 telnet 服务器嵌入到包含 JUnit 测试框架的 bundle 中去,从而通过调用 JUnit 已经实现的基于 Text 的 TestRunner,就可将结果输出到 telnet 的 Terminal 上。这样的方式提供了一种便捷的远程调用测试代码的方式,很适合用于一些服务器端程序的测试工作。

总结展望

在本文中,我们介绍了目前测试基于 OSGi/Eclipse 的应用程序时所遇到的一些困难,给出了一种基于 JUnit 的测试框架。这一测试框架对于熟悉 JUnit 和 OSGi 的编程人员来说,几乎无需学习就可以使用。同时作为一个轻量级的测试框架,我们给出了三种不同的实现方式,这三种方式各有利弊,针对不同的现实情况可以组合起来使用。开发人员还可以在此之上添加丰富的用户调用接口,使得在 OSGi 环境中的测试更为便捷、高效。

OSGi/Eclipse 框架由于其可扩展性和动态加载等特性得到越来越多的应用,同时由于其不同于传统 Java 应用程序的特性,使得许多应用于传统 Java 应用程序的开发和测试经验在这一框架中受到许多的限制。如何更为有效地利用这些积累下来的宝贵经验和实践方式,将是众多 OSGi/Eclipse 的开发测试人员面对的新挑战。


参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值