第12章 测试OSGi应用
在遵循最佳实践原则和使用Gemini Blueprint支持的情况下,你的bean类应该很容易进行单元测试,因为它们不强依赖于OSGi,你使用的少量OSGi API(例如BundleContext)是基于接口的,很容易模拟。不管你是否想要做单元测试或集成测试,Spring DM都可以减轻你的工作。
12.1. OSGi模拟
模拟与打桩:单元测试代码的策略有很多,最流行的策略就是打桩和模拟。参考Martin Fowler的文章this,他详细介绍了两者之间的区别。 |
虽然大多数的OSGi API都是接口,用专门的库(如EasyMock)创建模拟对象是相当简单的,但是实际上由于大量的设置代码,使得代码(尤其是JDK1.4)看起来很笨重。为了保持单元测试简短,Gemini Blueprint/Spring DM提供了OSGi模拟包org.eclipse.gemini.blueprint.mock。
用不用它们你说了算,然而,我们在Blueprint/Spring DM 测试套件中广泛的使用了它们。在我们的代码基线中你可以找到下面的代码片段:
private ServiceReference reference; private BundleContext bundleContext; private Object service; protectedvoid setUp() throws Exception { reference = new MockServiceReference(); bundleContext = new MockBundleContext() { public ServiceReference getServiceReference(String clazz) { return reference; } public ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException { returnnew ServiceReference[] { reference }; } public Object getService(ServiceReference ref) { if (reference == ref) return service; super.getService(ref); } }; ... } publicvoid testComponent() throws Exception { OsgiComponent comp = new OsgiComponent(bundleContext); assertSame(reference, comp.getReference()); assertSame(object, comp.getTarget()); } |
作为结束语,要尝试使用它们,并选择你最感觉最舒适的风格和库。在我们的测试套件中,我们将使用上述的模拟、EasyMock库和大量的集成测试。
12.2. 集成测试
JUnit4/TestNG怎么样? |
虽然JUnit4/TestNG解决了构建基本JUnit类时的类继承问题,通过注解将执行器与测试解耦,但是Gemini Blueprint/Spring DM不能使用它们,因为它必须支持Java 1.4。然而,将来可能提供一个可选的基于JVM 5的测试扩展,用上述的库集成已有的测试。 |
在受限环境中,例如OSGi,测试类、bundle的manifest的可见性和版本管理是很重要的。
为了减轻集成测试的负担,Gemini Blueprint工程提供了测试类等级(基于org.eclipse.gemini.blueprint.test.AbstractOsgiTests),它提供给了编写正规JUnit测试的支持,并且在OSGi环境中自动执行它们。
通常,Gemini Blueprint/Spring DM测试框架支持的场景有:
- 启动OSGi框架(Equinox, Knopflerfish, Felix)
- 安装和启动任何测试需要的bundle
- 将测试用例打包为一个在线的bundle,并生成manifest(如果没有提供) ,并将这个bundle安装到OSGi框架
- 在OSGi框架中执行测试用例
- 关闭OSGi框架
- 将测试结果返回给原始测试用例
| 警告 |
该测试框架的目标是在非OSGi环境(如Ant/Maven/IDE)运行OSGi集成测试。该测试框架不是作为OSGi bundle(它也不是那样运行的)。实际上,这里的意思是测试bundle应该与它测试的bundle分开(就类似于单元测试,要与它测试的类是分离一样) |
按照这个步骤,编写就Junit的OSGi集成测试并集成到任何环境(IDE、ant、maven等等)是很繁琐的。本章剩余部分详细介绍Gemini Blueprint/Spring DM测试套件的特定。
12.2.1. 创建一个简单的OSGi集成测试
虽然测试框架包含提供特定特性的类,但是大多数情况下你的测试用例只需继承org.eclipse.gemini.blueprint.test.AbstractConfigurableBundleCreatorTests(至少,这是实际中使用的)。
让我们继承这个类,并通过bundleContext字段与OSGi平台交互:
publicclassSimpleOsgiTestextendsAbstractConfigurableBundleCreatorTests { publicvoid testOsgiPlatformStarts() throws Exception { System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_VENDOR)); System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_VERSION)); System.out.println(bundleContext.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT)); } } |
简单的执行这个测试,就像你平常执行Junit测试一样。在Equinox 3.2.x上,输出类似于:
Eclipse
1.3.0
OSGi/Minimum-1.0,OSGi/Minimum-1.1,JRE-1.1,J2SE-1.2,J2SE-1.3,J2SE-1.4}
在测试执行期间,你可能会看到测试框架输出不同的日志语句。但是这些都是可以禁止的,因为它们只是告知信息,不影响实际执行。
注意,你不需要出具任何bundle,编写任何MANIFEST,或导入或导出,更不用启动和关闭OSGi平台。当测试执行时,测试框架会自动完成这些事。
让我们做一些查询,看看测试是在什么环境中运行的。一个简单的方式是向bundleContext查询安装的bundle:
publicvoid testOsgiEnvironment() throws Exception { Bundle[] bundles = bundleContext.getBundles(); for (int i = 0; i < bundles.length; i++) { System.out.print(OsgiStringUtils.nullSafeName(bundles[i])); System.out.print(", "); } System.out.println(); } |
输出类似于这样:
OSGi System Bundle, ObjectWeb ASM, log4j.osgi, spring-test, spring-osgi-test, spring-osgi-core,
spring-aop, spring-osgi-io, slf4j-api,
spring-osgi-extender, etc... TestBundle-testOsgiPlatformStarts-com.your.package.SimpleOsgiTest,
正如你所看到的,测试框架安装了运行测试需要的强制依赖,例如Spring、Gemini Blueprint/Spring DM、slf4j。
12.2.2. 安装测试前提条件
OSGi友好的库 |
要在OSGi环境中工作,jar文件需要在MANIFEST.MF中声明导入或导出的包,即声明需要从其它bundle获取哪些类,为其它的bundle提供那些类。大多数的库都不是OSGi的,不提供合适的manifest条目,这意味着它们在OSGi环境中是不可用的。现在,开源空间有几个方案提供了合适的manifest-请参见FAQ了解更多信息。 |
除了Gemini Blueprint/Spring DM的jar,测试自己可能依赖几个库或者自己的代码。
考虑下面的测试,它依赖于Apache Commons Lang:
import org.apache.commons.lang.time.DateFormatUtils; ... publicvoid testCommonsLangDateFormat() throws Exception { System.out.println(DateFormatUtils.format(new Date(), "HH:mm:ssZZ")); }} |
运行这个测试,会产生异常:
java.lang.IllegalStateException: Unable to dynamically start generated unit test bundle
...
Caused by: org.osgi.framework.BundleException: The bundle could not be resolved.
Reason: Missing Constraint: Import-Package: org.apache.commons.lang.time; version="0.0.0"
...
... 15 more
这个测试需要org.apache.commons.lang.time 包,但是没有bundle导出这个包。解决这个问题,安装commons-lang的bundle(确保你用的版本为2.4以上,因为它在manifest中添加了合适的OSGi条目)。
可以用getTestBundlesNames或 getTestBundles指定要安装那些bundle。前一个方法返回一个字符串数组,表示bundle的名字、包和版本,而后一个方法返回一个资源数组,可以安装bundle可以直接使用。即用getTestBundlesNames方法,你需要依赖别人帮你定位bundle(大多数场景),而getTestBundles方法,当你想要自己定位bundle的位置用。
默认情况下,测试套件会执行构建查找,类似于Maven2,首先搜索当前工程的相对位置,然后才去本地仓库。定位程序希望bundle字符串是逗号分隔的值,包含group、name、version和type(可选)。将来可能会有其他不同的定位程序。通过org.eclipse.gemini.blueprint.test.provisioning.ArtifactLocator接口也可以实现自己的定位程序。
让我们修改一下集成测试,安装必须的bundle(和一些额外的OSGi库):
protected String[] getTestBundlesNames() { returnnew String[] { "javax.transaction, com.springsource.javax.transaction, 1.1.0", "commons-lang, commons-lang, 2.4" }; }; } |
重新运行这个测试,应该显示出这些bundle都已经安装到了OSGi平台。
| 注意 |
上面提到的构件必须存在于maven的本地仓库 |
12.2.3. 测试框架的高级主题
测试框架允许进行定制。本章详细介绍已有的钩子。然而,它们是高级主题,它增加了测试基础的复杂性。
12.2.3.1. 自定义测试的manifest
有些场景,自动生成的manifest不满足测试需要。例如manifest需要不同的头或者需要导入特定的package。
对于简单场景,可以直接生成manifest-下面的例子中,指定了bundle的类路径:
protected Manifest getManifest() { // let the testing framework create/load the manifest Manifest mf = super.getManifest(); // add Bundle-Classpath: mf.getMainAttributes().putValue(Constants.BUNDLE_CLASSPATH, ".,bundleclasspath/simple.jar"); return mf; } |
另一种选择是覆写getManifestLocations()方法,提供自己的manifest文件:
protected String getManifestLocation() { return"classpath:com/xyz/abc/test/MyTestTest.MF"; } |
然而,每一个manifest需要下面的条目:
“Bundle-Activator: org.eclipse.gemini.blueprint.test.JUnitTestActivator”
因为如果没有没有这个条目,测试基础的功能就不正常。另外,还需要导入基础测试套件要用的JUnit、Spring和Gemini Blueprint/Spring DM特定的package:
Import-Package: junit.framework,
org.osgi.framework,
org.apache.commons.logging,
org.springframework.util,
org.eclipse.gemini.blueprint.service,
org.eclipse.gemini.blueprint.util,
org.eclipse.gemini.blueprint.test,
org.springframework.context
如果导入这些类失败,测试时就会抛出NoDefClassFoundError错误。
12.2.3.2. 自定义测试bundle的内容
默认情况下,对于运行中的bundle,测试基础用./target/test-classes文件夹下所有的类、xml和属性文件。这符合maven的文件布局。这些设置可以用两种方式配置:
- 以编程方式覆写AbstractConfigurableBundleCreatorTests的getXXX方法。
- 创建一个属性文件,名字与测试用例类似。例如com.xyz.MyTest的属性文件命名为com/xyz/MyTest-bundle.properties。如果找到了这个文件,那么就会从文件中读取下面的属性:
表12.1. 默认的测试jar内容设置
属性名 | 默认值 | 描述 |
root.dir | file:./target/test-classes | Jar的根目录 |
include.patterns | /**/*.class,/**/*.xml,/**/*.properties | 逗号分隔的Ant风格的模式字符串 |
manifest | (empty) | Manifest位置,是一个字符串。默认情况下,它是空的,意思是由测试框架创建,而不是由用户提供 |
这个选项是很方便的,当你创建特定的测试时,需要包含特定的资源(例如本地化文件或者图片)。请查阅AbstractConfigurableBundleCreatorTests和AbstractOnTheFlyBundleCreatorTests测试类了解更多的钩子。
12.2.3.3. 理解MANIFEST.MF生成
测试框架的非常有用的特性就是基于bundle内容自动生成测试manifest。Manifest创建者组件使用字节码分析器来确定测试类导入的package,这样它就可以生成合适的OSGi指令。由于生成的bundle是用于测试,因此创建者会用下面的假设:
- 没有package被导出
运行中的bundle用于运行单元测试,通常会消费OSGi的package。这个行为可以通过自定义manifest修改。
- 不支持分离package (即来自不同bundle相同package的类)
这意思是说测试框架中的package是完整的,不会为它们生成Import-Package条目。为了避免这个问题,使用子package或将类移动到同一个bundle。注意不鼓励使用分离package,因为它们会出很多问题。
- 测试bundle只包含测试类
字节码解析器只看测试类的层次,任何其它的类都不会包含到bundle中,不会生成导入语句。若要修改默认行为,覆写createManifestOnlyFromTestClass,返回false:
protected boolean createManifestOnlyFromTestClass() {
return false;
}
| 注意 |
生成manifest花费的时间会随着bundle类的数量和大小而增长 |
此外,自定义manifest或者以内部类的方式为你测试类附加代码(这样它可以自动解析)。“lack of such features”的原因是字节码解析器简单快速出击测试的manifest-这不是创建OSGi构建的通用工具。
12.2.4. 创建OSGi应用上下文
Gemini Blueprint/Spring DM测试套件是在Spring测试类上构建的。要创建一个应用上下文(特定OSGi的),应该覆写getConfigLocations[]方法,指明应用上下文配置的位置。在运行时,创建的OSGi应用上下文在测试用例的生命周期内会被缓存起来。
protected String[] getConfigLocations() {
return new String[] { "/com/xyz/abc/test/MyTestContext.xml" };
}
12.2.5. 指定要使用的OSGi平台
测试框架支持即拆即用,三个OSGi 4.0的实现,即Equinox、Knopflerfish和Felix。要使用它们,必须把它们放到类路径中。默认情况下,测试框架尝试用Equinox平台。这可通过三种方式配置:
- 通过getPlatformName()方法以编程方式配置
覆写上述方法,指定一个平台接口实现的全限定名。用户可以使用Platforms类指定一个平台:
protected String getPlatformName() {
return Platforms.FELIX;
}
- 通过系统属性org.eclipse.gemini.blueprint.test.framework声明
如果这个属性设置了,那么测试框架就使用它的值作为平台实现的全限定名。如果失败了,在记录一条警告消息的日志之后,则会回到Equinox。这对于构建工具是很有用的(例如maven),因为它指定一个了特定的目标环境,而无需修改测试代码。
12.2.6. 等待测试依赖
测试框架有一个内置的特性,就是在执行测试之前,能够等待所有的依赖都被部署。由于OSGi平台天生就是并发的,安装一个bundle并不意味着他的服务都是运行的。在依赖的服务初始化完成之前,运行测试会引发不定时发生的错误,而污染了测试结果。默认情况下,测试框架会检查用户安装的bundle,如果是基于Spring的bundle,等待,直到它们完全启动(即它们的应用上下文作为OSGi服务发布)。这个行为可以通过覆写shouldWaitForSpringBundlesContextCreation方法来禁止。查阅AbstractSynchronizedOsgiTests了解更多细节。
12.2.7. 测试框架的性能
考虑测试框架提供的所有功能,你可能会疑惑是否有性能瓶颈。首先,值得注意的是,测试基础自动完成的工作无论如何都是完成的(例如创建manifest或者创建测试bundle,或者按照bundle)。手动完成这些工作很容易导致错误和时间消耗。实际上,当前的测试基础启动很高效的自动测试,无需担心部署问题和冗余。
至于数字,当前的测试基础设施已经在内部使用了一年半的时间-我们的集成测试(大约120个)在笔记本上运行大约3:30分钟。大多数的时间都消耗在启动和停止OSGi平台上:测试框架花费大约10%的时间(目前在我们的性能分析中是这样)。例如,manifest生成花费的时间不到0.5s,而jar生成大约为1s。
然而,我们正在努力让它更快,更智能,这样需要更少的配置,上下文信息尽量被使用。如果你有任何想法或建议,请使用我们定位entire跟踪和论坛。
希望这篇文章能够说明Gemini Blueprint/Spring DM测试基础设施如何简化OSGI集成测试,如何定制化。请查阅javadoc了解更多信息。