单元测试框架NUnit 之 Extensibility可扩展性

你可以通过以下几种方法扩展Nunit:通过custom constraints对我们测试项目引用的Nunit framework进行扩展,针对我们自己的测试项目;通过addin对Nuint Core扩展,这样可以影响Nuint对测试项目的编译运行,此外,还可以对GUI运行工具的扩展。

Custom Constraints (NUnit 2.4 / 2.5)

通过继承抽象类Constraint,你可以实现自定义约束custom constraints 它会一个真实值进行一个测试并且产生适当的提示信息。

public abstract class Constraint

{

 	...

    public abstract bool Matches( object actual );

    public virtual bool Matches( ActualValueDelegate del );

    public virtual bool Matches<T>( ref T actual );

    public abstract void WriteDescriptionTo( MessageWriter writer );

    public virtual void WriteMessageTo( MessageWriter writer );

    public virtual void WriteActualValueTo( MessageWriter writer );

	...

}

这个类不只是上面所列出的,可供你扩展的包括2个你必须实现的抽象abstract的方法和4个虚拟的方法,它们包含默认实现,你可以根据自己的意思是否去重写它们。继承的子类应该保存用于匹配的真实值在一个受保护的protected的actual字段中以备后用。其中的MessageWriter是一个抽象类,它被TextMessageWriter类实现。查看内置约束的代码,我们可以知道如何去自定义错误信息。

下面是一个Nuint中EmptyStringConstraint类的实现:

/// <summary>

    /// EmptyStringConstraint 用来测试一个字符串是否为空

    /// </summary>

    public class EmptyStringConstraint : Constraint

    {

        /// <summary>

        /// 测试给定的值是否满足约束

        /// </summary>

        /// <param name="actual">要测试的值</param>

        /// <returns>测试成功返回True,失败返回false</returns>

        public override bool Matches(object actual)

        {

            //  真实值保存到基类的actual字段中,以备后用

            this.actual = actual;



            if (!(actual is string))

                return false;



            return (string)actual == string.Empty;

        }



        /// <summary>

        /// 向MessageWriter写入约束描述

        /// </summary>

        /// <param name="writer">表现描述的writer</param>

        public override void WriteDescriptionTo(MessageWriter writer)

        {

            writer.Write("<empty>");

        }

    }

  

自定义约束的语法实现:Nunit本身有一些类包含特定约束的语法实现。当然,Nuint并不会为自定义约束实现语法实现,除非我们自己实现。不过,我们可以用Matches(Constraint) 语法元素来这样写代码:

MyConstraint myConstraint = new MyConstraint();

Assert.That( myArray, Has.Some.Matches(myConstraint) );

Nunit Addins

Nunit最早识别测试是通过继承测试基类和以test开头的方法。但是从2.0开始以特性标记的方式。但是移除了继承子类的机制,我们也失去了一个简单的扩展Nunit内部行为方法。此时,addins填补这个空白,使我们不用修改Nuint本身就可以引入新的行为或修改原来的行为。

Nunit提供了几个扩展点,因为Nunit运行在不同的host和应用不同的应用程序域去运行测试,可以分为三种类型的扩展点:Core,client和gui。也就是我们可以在这三个方面对Nunit进行扩展。

通过插件addin,你可以在任一个扩展点添加多个扩展。每个扩展都必须标明扩展类型,因此扩展host知道如何激发它们:目前的版本中只支持core类型的扩展。

Nunit检查在bin/addins目录(实际上是在和lib同级的addins目录下)下的所有程序集,寻找标记NUnitAddinAttribute并且实现IAddin接口的公共类,然后做为插件加载它们。

NUnitAddinAttribute支持三个可选的命名参数:Type,Name和Description。Name和Description都是字符串形式标明扩展的名字和描述。如果名字没提供,默认使用类名做为名字。Type可以一个或者ExtensionType的组合:

	[Flags]

	public enum ExtensionType

	{

		Core=1,

		Client=2,

		Gui=4

	}

如果类型也没有提供,默认是ExtensionType.Core。

每个插件都必须实现IAddin:

        public interface IAddin

	{

		bool Install( IExtensionHost host );

	}

这个Install方法会被它本身标记类型的host调用,插件应该检查必要的扩展点是否可用然后安装自己,安装成功返回true,失败返回false。这个方法只会被每个扩展host和一个新的测试应用程序域加载时调用一次。

Install方法用IExtensionHost接口定位扩展点:

        public interface IExtensionHost

	{

	 	IExtensionPoint[] ExtensionPoints { get; }

		IExtensionPoint GetExtensionPoint( string name );

		ExtensionType ExtensionTypes { get; }

	}

ExtensionPoints属性返回这些扩展需要的所有扩展点的数组。ExtensionTypes属性返回当前host支持的扩展类型的标识,例如Gui扩展只会被Gui host加载。目前,继承自IExtensionHost的抽象类ExtensionHost实现了这几个方法,也只有CoreExtensions继承自ExtensionHost,因此目前只有Core类型的扩展被支持。

大多数的插件只用到GetExtensionPoint方法去获取一个特定的扩展点的接口,IExtensionPoint定义如下:

        public interface IExtensionPoint

	{

		string Name { get; }

		IExtensionHost Host { get; }

		void Install( object extension );

		void Remove( object extension );

	}

大多数插件只调用Install方法,它传入一个扩展对象给它要被安装的扩展点。一般情况下,扩展一旦安装不必移除,但是它仍然被提供以防万一。抽象类ExtensionPoint继承此接口并且实现了这几个方法,而它又被SuiteBuilders,TestCaseBuilders,TestDecorators,TestCaseProviders,DataPointProviders,EventListeners这些类继承,而这些就是实际的扩展点。

在2.5中,另一个接口,一个继承自IExtensionPoint的IExtensionPoint2被引入,它允许你设定在同一个扩展点相对于其它扩展调用的顺序:

public interface IExtensionPoint2 : IExtensionPoint

	{

		void Install( object extension, int priority );

	}

在2.5版本中,只有TestDecorators和DataPointProviders扩展点实现了这个接口。

对于不同的扩展点,这个传入的对象应该继承一个或多个不同的接口。

1,SuiteBuilders (NUnit 2.4)

SuiteBuilder是一个把类构建为测试类的插件。Nunit本身就是利用一个SuiteBuilder识别和构建测试类的。

在插件中我们可以用host通过名字来获取此扩展点对象:

IExtensionPoint suiteBuilders = host.GetExtensionPoint( "SuiteBuilders" );

如果实现对该扩展点扩展,传给Install方法的扩展对象必须实现ISuiteBuilder接口:

public interface ISuiteBuilder

	{

		bool CanBuildFrom( Type type );

		Test BuildFrom( Type type );

	}

CanbuilderFrom应该返回true,如果builder能从指定类型构建测试类,它通常要检查类型和它的特性。BuildFrom应该返回一个包含测试方法的测试类,如果不能构建测试类时返回null。

2,TestCaseBuilders (NUnit 2.4)

TestCaseBuilders是基于方法来创建测试的。Nunit在内部就是用几个TestCaseBuilders来创建各样的测试方法的。

在插件中通过以下代码来获取此扩展点对象:

IExtensionPoint testCaseBuilders = host.GetExtensionPoint( "TestCaseBuilders" );

对该扩展点的扩展类要实现以下两个接口中的一个:

public interface ITestCaseBuilder

	{

		bool CanBuildFrom( MethodInfo method );

		Test BuildFrom( MethodInfo method );

	}



	public interface ITestCaseBuilder2 : ITestCaseBuilder

	{

		bool CanBuildFrom( MethodInfo method, Test suite );

		Test BuildFrom( MethodInfo method, Test suite );

	}

Nunit会先调用ITestCaseBuilder2 ,如果不合适调用ITestCaseBuilder。

CanBuildFrom应该返回True,如果插件能从提供的方法中构建测试。一些TestCaseBuilder插件只能在应用到特定测试类中的方法中,ITestCaseBuilder2 接口中的suite参数就是用来做这个决定的。BuildFrom方法应该返回传入参数构建的测试方法,如果传入的方法不能用时返回null。

3,TestDecorators (NUnit 2.4)

TestDecorators能对构建生成的测试进行修改。

插件可用以下代码获取该扩展点对象:

IExtensionPoint testDecorators = host.GetExtensionPoint( "TestDecorators" );

传给Install的扩展对象就实现以下接口:

public interface ITestDecorator

	{

		Test Decorate( Test test, MemberInfo member );

	}

这个Decorate方法可以做:什么也不做,直接返回;修改测试对象的属性,然后返回;舍弃测试对象或把它整合到新的对象中。

根据decorator的需要,它可能要比其它decorator之前或之后运行,decorator可以用重载的Install方法,传入一个优先级标识,这个值从1到9,数值越小,级别越高。以下值的定义,用到的时候可以调用:

  • DecoratorPriority.Default = 0
  • DecoratorPriority.First = 1
  • DecoratorPriority.Normal = 5
  • DecoratorPriority.Last = 9

4,TestCaseProviders (NUnit 2.5)

TestCaseProviders是和带参的测试一起使用,为带参测试调用时创建测试用例。

插件通过以下代码获取扩展点对象:

IExtensionPoint listeners = host.GetExtensionPoint( "ParameterProviders" );

传给Install方法的对象应实现以下两个接口的一个:

public interface ITestCaseProvider

	{

		bool HasTestCasesFor( MethodInfo method );

		IEnumerable GetTestCasesFor( MethodInfo method );

	}

	

	public interface ITestCaseProvider2 : ITestCaseProvider

	{

		bool HasTestCasesFor( MethodInfo method, Test suite );

		IEnumerable GetTestCasesFor( MethodInfo method, Test suite );

	}

如果2不适合时,就调用1接口。

HasTestCasesFor 返回True,如果这个provider对于给定的方法能提供测试用例。如果 provider 只有被应用在特定的测试中,它检查suite参数来决定是返回true或false。

GetParametersFor方法应该返回一系列独立的测试,每个测试表现为一个ParameterSet类型的对象或参数的数组或自定义的包含以下属性的对象:

  • Arguments
  • RunState
  • NotRunReason
  • ExpectedExceptionType
  • ExpectedExceptionName
  • ExpectedExceptionMessage
  • Result
  • Description
  • TestName

ParameterSet类也提供这些属性,以供调用。

注意:

a,如果你实现了两个接口,大部分的provider会用其中的一个代理另一个。

b,如果provider要使用测试类的数据,请使用ITestCaseProvider2,以确保它能够调用测试类构造时的参数。

c,从测试类外部获取数据的provider只调用ITestCaseProvider。

d,ITestCaseProvider2在2.5.1版本中才加入的。

5,DataPointProviders (NUnit 2.5)

DataPointProviders以独立方式为带参数的测试方法提供数据。

插件中用以下代码获取此扩展点对象:

IExtensionPoint listeners = host.GetExtensionPoint( "DataPointProviders" );

传给Install方法的扩展对象就实现以下两个接口中的一个:

public interface IDataPointProvider

	{

		bool HasDataFor( ParameterInfo parameter );

		IEnumerable GetDataFor( ParameterInfo parameter );

	}

	

	public interface IDataPointProvider2 : IDatapointProvider

	{

		bool HasDataFor( ParameterInfo parameter, Test parentSuite );

		IEnumerable GetDataFor( ParameterInfo parameter, Test parentSuite );

	}

仍然优先采用2.

如果对于指定的参数,provider能提供数据,HasDataFor就返回true。如果只要被特定测试调用,它应该检查提供的参数、关联的方法和提供的parentSuite参数。GetDataFor返回运行测试的一系列独立的数据。

ITestCaseProvider中要注意的几点同样适用于IDataPointProvider。

6,EventListeners (NUnit 2.4.4)

EventListeners用来对测试运行中发生的事件做出反应,通常用来记录信息。这个事件监听调用和测试的运行是异步的,不会对实际的运行产生影响。

插件中可以通过以下方式获取该扩展点对象:

IExtensionPoint listeners = host.GetExtensionPoint( "EventListeners" );

传给Install的扩展对象应实现以下接口:

	public interface EventListener

	{

		void RunStarted( string name, int testCount );

		void RunFinished( TestResult result );

		void RunFinished( Exception exception );

		void TestStarted(TestName testName);

		void TestFinished(TestResult result);

		void SuiteStarted(TestName testName);

		void SuiteFinished(TestResult result);

		void UnhandledException( Exception exception );

		void TestOutput(TestOutput testOutput);

	}

你必须提供所有的方法,但可以为空。

扩展的一些建议

首先,Nunit官方文档不建议现在就进行扩展开发,而是希望开发者提倡加入开发或者提出问题和建议给他们。但是,他们仍然列出了一些建议:

1,相对于nunit.core 各个版本中都会有改变,nunit.core.interfaces 是相对稳定的。虽然这两个程序集都还不是定型,但是仅依赖于interfaces的扩展在以后的各个版本中会更易于使用。不幸的是,这样就不能复用Nunit的代码,工作起来会更加困难。现在大部分的插件示例都是针对特定版本的。

2,如果你把一个自定义的特性和你的插件放到了一个程序集,因此用户测试就会依赖这个程序集。如果插件是依赖版本的,那么用户的测试也就依赖版本了。因此,你应该把用户测试需要引用的类放到独立的程序集中,特别是你的扩展依赖nunit.core时。

3,如果使用vs,把任何对nunit.core或nunit.core.interfaces的引用都设成Copy Local为false。如果你也需要自己编译NUnit,这是非常重要的。

4,现在没有方法让decorators按一定的顺序应用。Nunit按反射返回的次序来应用它们,这在不同的运行时是不一样的。

5,不要对已有扩展点做本身要求之外的扩展,相信Nunit以后会提供更易用的扩展点,也可以给Nunit官方提建议。

 

扩展介绍完毕,下面将列出一个Nunit官方的扩展的例子。

你可能感兴趣的:(单元测试)