.Net的Attribute对单元测试的影响
----------------------------
高巍(网名DrCMM,w-gao@263.net) 编译
本文欢迎转载,欢迎大家与我交流讨论。转载时请保留上述声明,谢谢!
----------------------------
说明:
拜读了shanyou的《在.NET环境中使用单元测试工具NUnit》,正好IEEE Software杂志上有一篇分析attribute的好文章,改编整理之后贴出,供各位网友一并参考。
又Property、Attribute一般都译为“属性”,为避免混淆,此处attribute均保留不译。
Microsoft在.Net框架中引入了attribute,这是一种给运行时实体附加“声明性信息(declarative information)”,也称元数据的方法。可以附加attribute的实体包括:类、方法、property、类变量等。在.Net中,还可以给assembly附加attribute,不同类型的attribute描述了assembly不同方面的信息。如:身份类attribute用来描述该assembly的识别特征(名称、版本等),信息类attribute用来提供更多的产品与公司信息,声明类attribute用来描述配置信息,强名称attribute用来描述assembly是否使用了公共密钥加密的签名。应用程序能够在运行时读取这些信息,根据这些信息来控制自身与诸如序列化、安全等服务的交互作用方式。
.Net的Attribute与Java的标记接口(Marker Interface)
标记接口(Marker Interface)是Java中的一个常见设计技巧。所谓标记接口是一个不包含任何method或field的接口,它只有一个用途,就是便于Java虚拟机(JVM)识别一个类是否具有某个特定的attribute。如下例:
public interface Serializable {}
我们编写的类如果要求能够序列化,就必须实现这个接口:
public class SerializableClass implements Serializable
作为类的开发人员,我们有时候需要控制与序列化相关的某些行为。然而,在Java中,Serializable作为代表了序列化契约的接口,它与这些行为之间却并没有被显式地关联起来。当程序在运行时请求JVM对一个类序列化,JVM就查看该类是否实现了Serializable接口,同时还查看该类是否定义了但没有直接声明诸如readResolve、 readObject、 或writeObject等与序列化接口相关的方法。JVM是靠命名规范和方法原型,通过reflection来定位这些方法,一旦找到即调用之。但是,Serializable接口本身却并不显式地指定这些方法。因为,需要序列化的类在实现该接口时,有可能以最简形式来实现这些方法,而这是完全没有必要的。接口中不显式地指定与之相关的方法,那么,这些方法的原型就有可能在他处被错误地指定。可见,Java的这种处理机制比较容易出错。更糟的是,编译时检查也无法将之作为错误而识别出来。
.Net对这个问题的解决之道就是显式声明(“有了快感你就喊”?^_^)。最简单的例子,假设程序员需要利用系统提供的序列化功能来序列化一个对象,就可以用Serializable这个类级别的attribute来标记该类,声明该类拥有系统提供的序列化功能。如下例:
[Serializable()]
public class MyClass {}
仅仅标记一个类可序列化并不能做任何事情。如果程序员需要完全控制序列化的过程,就必须实现一个ISerializable的接口,指定用于控制序列化过程的方法。如下例:
[Serializable()]
public class MyClass : ISerializable
{
public MyClass(
SerializationInfo info,
StreamingContext context)
{
// ...
}
public void GetObjectData(
SerializationInfo info,
StreamingContext context)
{
// ...
}
}
在运行时,如果一个程序请求CLR(公共语言运行库)序列化一个类,CLR就查看该类是否被标记为具备Serializable的attribute。我们可以看到,Java与.Net在处理这一问题时的方式颇有“英雄所见略同”之意。但.Net对attribute的运用显得更直截了当,代价是引入了一个新的语言结构(Language Construct)。Java则复用了一个现有的语言结构——接口,通过称为“标记接口”的设计技巧也达到了attribute同样的功能,表达了attribute可以表达的信息。Stroustrup在《C++语言的设计与演化》中提到,C/C++社群中有尽量不引入新的语言结构而通过某些设计技巧来重用已有的语言结构的倾向,每个新的关键字的引入都颇费斟酌。这实际上是程序语言的不同设计理念与指导哲学,需要在程序语言追求紧凑干净因而表达某些常用语义都要求较高的设计技巧,和程序语言不惜庞大繁复使得常用语义都有自带的现成语言结构可以表达但难学难用、臃肿不堪之间寻求一个恰当的平衡。
格式化命名模式(Stylistic Naming Patterns)
Java通常使用命名规范来识别一个特定的方法。程序在运行时使用reflection,通过方法名就能定位一个方法。 一旦找到,程序就可以调用执行之。例如,在开源单元测试框架JUnit中,程序员定义一个测试方法时,该方法的名称必须以test开头。执行测试的程序首先验证这个类继承自TestCase,然后,使用reflection来查找以test开头的所有方法。如下例:
public class MyClass extends TestCase
{
public void testSuccess()
{ /* ... */ }
}
程序员如果需要验证代码是否抛出异常,在JUnit中可以使用如下所示的一个常用的设计惯例:
public class MyClass extends TestCase
{
public void testMyException()
{
try {
/* code that throws exception */
fail(“Code should have thrown MyException”);
}
catch(MyException e)
{ /* expected exception — success */ }
}
}
这个设计惯例并不直观,而且在每个期待异常出现的测试用例中都要加上这么一段代码,既重复又繁琐。这是一种常见的情形,也许我们可以让JUnit框架来对此提供直接支持。但是,依靠命名规范来识别一个方法还会导致更糟的情形,如下例:
public class MyClass extends TestCase
{
public void testSuccess_ExpectException_MyException()
{ /* ... */ }
}
在上面的例子中,我们通过命名规范的使用来说明:这是一个测试方法,而且我们期待该方法执行后会抛出MyException的异常。可以看到,这么多的额外信息要让方法名来传递确实有点“生命不可承受之重”的勉为其难。上面的例子虽然有点极端,但它的确揭示了命名规范的应用是有很大限制的,名称本身是难以承载过多的信息。实际上,JUnit并不采用这种方法来实现检查边界条件的功能。Java中还有其他方法(如JavaDoc的tag)可以起到附加信息的作用,但它们不是在运行时出现的,而且它们通常要求对代码进行预处理以识别和处理这些tag。
在.Net中,格式化命名模式就完全不需要了。因为除了.Net框架提供的attribute之外,程序员还可以创建自己的客户化定制attribute。客户化定制attribute的定义与使用和系统提供的attribute完全相同。这些attribute并不只是名称而已,它们是类的实例,可以带有附加数据。我们再来看NUnit,它是JUnit在.Net平台上的变种(derivative),支持.Net平台上各种语言的单元测试。NUnit在类和方法的层次上使用了attribute,类attribute称为TestFixture,方法attribute称为Test。如下例,测试执行程序将查找attribute为TestFixture的类中attribute为Test的方法。这个整体解决方案有一气呵成、毫无断裂之感。
[TestFixture]
public class MyClass
{
[Test]
public void Success()
{ /* ... */ }
}
不仅于此,这一解决方案的可扩展性更强,因为对一个方法可以有不止一个的attribute,而且attribute还可以拥有附加的数据。例如,NUnit还有一个方法attribute,表示该方法执行后会抛出一个异常。这不仅使得方法名可以不受限于运行环境,而且与待测内容更为相关。如下例:
[TestFixture]
public class MyClass
{
[Test]
[ExpectedException(typeof(MyException))]
public void Success()
{ /* would throw my exception */ }
}
.Net中的attribute是一种给运行时实体附加声明性信息的更为优雅、一致的方法。因为运行时实体与提供支持的服务是通过声明性信息来交互的,这些服务以及attribute(即声明性信息的表示)就不必是固定不变的。通过使用客户化定制attribute,.Net提供了一种标准的机制来扩展系统内建的元数据,程序员就能开发出可以与CLR不支持或尚未定义的服务进行交互的应用程序。实际上,NUnit2.0就是使用客户化定制attribute编写的,从而具备了我们提到的种种灵活性。与.Net的attribute解决方案相比而言,Java中附加声明性信息的常见方法,如标记接口、格式化命名模式和JavaDoc的Tag等,既缺乏一致性、容易出错,对当今日益复杂的应用来说也显得过于简单。Java社群已经意识到了这一问题,JSR-175为Java平台规定了一个与.Net已有的attribute类似的特性。