单元测试框架NUnit 之 constraints 约束
从2.4之后,NUnit就采用了新的“基于约束”的模型,所有的断言都是在约束的基础上的来实现的。正如前文所说的,我们可以用同一个方法和不同的约束实现不同的断言。
这个方法,有相当数量的重载版本:
Assert.That( object actual, IResolveConstraint constraint );
如果你继承自AssertionHelper类实现自己的test fixture class , 可以用 Expect() 方法替代 of Assert.That()...
Expect( object actual, IResolveConstraint constraint );
我们可以看到它们都要求一个IResolveConstraint 的参数,这就是我们所谓的约束对象:
Assert.That( myString, new EqualConstraint("Hello") );
第二个参数是相等约束对象,上面的代码可以实现相等断言相同的功能,相等断言就是在此基础上实现的。
NUint了提供了一个语法帮助类来实现约束:
Assert.That( myString, Is.EqualTo("Hello") );
下面来看一下,NUnit内置的具体的约束对象:
1,Equal Constraint (NUnit 2.4 / 2.5)
相等性约束是用来测试真实值与你在约束构造中传入的期望值是否相等的,当然有时你也可以另外指定一个误差值。
构造器:
EqualConstraint(object expected )
语法帮助类,也会创建相等性约束:
Is.EqualTo( object expected )
你如果认为基于约束的模型就只是在调用或内部实现上做了一些重构那就错了,引入了这个模型之后,更重要和方便的是在约束对象上应用大量的修改器来定制来实现自己的特殊要求:
...IgnoreCase ...AsCollection ...NoClip ...Within(object tolerance) .Ulps .Percent .Days .Hours .Minutes .Seconds .Milliseconds .Ticks ...Using(IEqualityComparer comparer) ...Using(IEqualityComparer<T> comparer) ...Using(IComparer comparer) ...Using(IComparer<T> comparer) ...Using(Comparison<T> comparer)
例如:
Assert.That( myNumber, Is.EqualTo( 5 ).Within(0.075));
这样的调用倒是越来越常见的出现些这些开源的构架中了,上面的调用只要myNumber与5的差值在0.075之内,这个测试就是成功的。
上面提供的丰富的修改器可以让我们在数值、字符串、时间的比较中达到理想的效果。
数组和集合的比较:从2.2开始,Nunit就可以比较一维的数组了,从2.4开始,多维、交错数组、集合被支持比较了,从2.5开始,只要实现了IEnumerable都可以用来比较。它们维度、长度相同并且对应元素都相等时,才是相等的。你如果想比较两个不同形状的数组,可以用AsCollection的修改器,这样它们就会被一个个元素进行比较,而不考虑它们的维度和长度。但是交错数组不是一个单独的集合,它会把修改器应用在每个单独的数组上,但是这个大多情况下是没有效果的。
Dictionaries字典集合的比较:Dictionaries实现了ICollection,从2.4以后也被支持比较操作,但是住住不能得到有用的结果,因为它要求按照一样的顺序来比较的,这其实要求我们的对字典集合的操作是一致的。从2.5.6开始,Nunit有了特殊的实现来比较字典集合:它只要键值是一样的(不考虑顺序),各个键对应的值相等。
如果Nunit和.NET的比较不能达到你的要求,你也可以通过Using修改器应用你自己的规则。当和EqualConstraint一块使用时,你必须实现IEqualityComparer, IEqualityComparer<T>,IComparer, IComparer<T>; or Comparison<T> 做为参数传给Using。
Assert.That( myObj1, Is.EqualTo( myObj2 ).Using( myComparer ) );
细节:
1,当你比较两个自己编写对象的相等时,Nunit会调用对象上重写的Equals方法,但是如果你没有重写,你在不同的实体比较时会得到失败。实际上,只重写operator==而没重写Equals也是不起作用的。
2,Within 修改器原来只是会浮点型数据的比较实现的,从2.4开始,才支持用Timespan来支持DateTime类型,从2.4.2开始,其它数字类型也开始被支持。
3,从2.4.4开始,如果没有显式指定误差,float和double的比较,会应用GlobalSettings.DefaultFloatingPointTolerance,如果这个值就没设置,会使用0.0d。
4,2.2.3之前,两个NaN数值的比较会失败,但是之后它为成功。为了避免迷惑,请在适当的时候使用Is.NaN。
5,当两个字符串的比较失败时,运行器提示的信息时会在反馈的错误信息中用到两个字符串的值,但是字符过长时会被截断。从2.4.4开始你可以约束上使用NoClip的标志修改器,来显示全部的字符。除此之外TextMessageWriter.MaximumLineLength可以用来修改这个值。
6,当和数组、集合或字典一起使用时,EqualConstraint会执行递归操作,修改器会被就应用到它们的单个元素上。
7,使用EqualConstraint时,如果两个参数其中任一个为null时,用户指定的比较器不会被调用,比较按以下规则:如果两个都为null,比较成功,如果只有一个为null,则比较失败。
8,Nunit在比较Streams 和 DirectoryInfos时有不同的实现:Streams的内容会被比较,而DirectoryInfos,只为比较它的第一级子目录的内容。
2,Same As Constraint (NUnit 2.4)
SameAsConstraint用来测试实际值与约束构造时传入的值是不是同一个引用。
//构造方式 SameAsConstraint( object expected ); //语法方式 Is.SameAs( object expected )
3,Condition Constraints (NUnit 2.4)
ConditionConstraints 提供一系列方法,来测试传入的条件是否符合方法名字所表达的逻辑。
//NullConstraint,测试值是否为null //构造函数 NullConstraint(); //语法实现 Is.Null Is.Not.Null //TrueConstraint,测试值是否为true //构造函数 TrueConstraint(); //语法实现 Is.True //NaNConstraint,测试值是否为NaN //构造函数 NaNConstraint(); //语法实现 Is.NaN //EmptyConstraint,测试字符串,集合或字典是否为空. //构造函数 EmptyConstraint(); //语法实现 Is.Empty //UniqueItemsConstraint,测试数组、集合或者其它的IEnumerable的对象是不是有一些惟一的项组成的 //构造函数 UniqueItemsConstraint() //语法实现 Is.Unique
另外,EmptyConstraint会根据参数的类型来创建或使用EmptyStringConstraint,EmptyDirectoryConstraint 或者 EmptyCollectionConstraint 的实例.如果测试一个目录是否为空,你必须先构造 DirectoryInfo 对象。
4,Comparison Constraints (NUnit 2.4 / 2.5)
Comparison constraints测试两个数值的大小,它能够比较所有实现了IComparable的接口类型和所有的数值类型,2.5之后,IComparable<T>接口也被支持。从2.5开始,你也可以使用Using修改器来实现自己的比较逻辑。
//可使用的修改器 ...Using(IComparer comparer) ...Using(IComparer<T> comparer) ...Using(Comparison<T> comparer) //GreaterThanConstraint,测试一个值是否大于另一个 //构造器 GreaterThanConstraint(object expected) //语法实现 Is.GreaterThan(object expected) Modifiers //GreaterThanOrEqualConstraint,大于等于 //构造函数 GreaterThanOrEqualConstraint(object expected) //语法实现 Is.GreaterThanOrEqualTo(object expected) Is.AtLeast(object expected) //LessThanConstraint,小于 //构造函数 LessThanConstraint(object expected) //语法实现 Is.LessThan(object expected) //LessThanOrEqualConstraint,小于等于 //构造函数 LessThanOrEqualConstraint(object expected) //语法实现 Is.LessThanOrEqualTo(object expected) Is.AtMost(object expected) //RangeConstraint,判断是否在某个范围内 //构造函数 RangeConstraint(IComparable from, IComparable to) //语法实现 Is.InRange(IComparable from, IComparable to)
5,Path Constraints (NUnit 2.5)
Path constraints只比较路径,而不涉及文件和目录。它可以比较不同的文件系统的路径形式,因为比较之间会被转化成标准的形式。更多的时候我们不知道路径的形式,可以为IgnoreCase 和 RespectCase 修改器替代默认的方式。
//SamePathConstraint //构造函数 SamePathConstraint( string expectedPath ) //语法实现 Is.SamePath( string expectedPath ) //示例 Assert.That( "/folder1/./junk/../folder2", Is.SamePath( "/folder1/folder2" ) ); Assert.That( "/folder1/./junk/../folder2/x", Is.Not.SamePath( "/folder1/folder2" ) ); Assert.That( @"C:\folder1\folder2", Is.SamePath( @"C:\Folder1\Folder2" ).IgnoreCase ); Assert.That( "/folder1/folder2", Is.Not.SamePath( "/Folder1/Folder2" ).RespectCase ); //SubPathConstraint,测试一个路径是不在期望的路径之下 //构造器 SubPathConstraint( string expectedPath ) //语法实现 Is.SubPath( string expectedPath ) //SamePathOrUnderConstraint,相同或者在另一路径之下 //构造器 SamePathOrUnderConstraint( string expectedPath ) //语法实现 Is.SamePathOrUnder( string expectedPath )
6,Type Constraints (NUnit 2.4)
// 测试对象的真实类型 ExactTypeConstraint( Type ) Is.TypeOf( Type ) // 测试对象是不是类型的实例 InstanceOfTypeConstraint( Type ) Is.InstanceOfType( Type ) //测试一个类型是不是可能从Type中转化过去 AssignableFromConstraint( Type ) Is.AssignableFrom( Type )
7,String Constraints (NUnit 2.4)
只能用来比较字符串,如果比较其它类型会抛出异常。
//SubstringConstraint,测试字符串是否包含expected期望字符串 //构造器 SubstringConstraint(string expected) //语法 Is.StringContaining(string expected) Contains.Substring(string expected) [Obsolete] Text.Contains(string expected) [Obsolete] Text.DoesNotContain(string expected) //StartsWithConstraint,是不是以expected开始的 StartsWithConstraint(string expected) Is.StringStarting(string expected) StartsWith(string expected) [Obsolete] Text.StartsWith(string expected) [Obsolete] Text.DoesNotStartWith(string expected) //EndsWithConstraint,是不是以expected结尾 EndsWithConstraint(string expected) Is.StringEnding(string expected) EndsWith(string expected) [Obsolete] Text.EndsWith(string expected) [Obsolete] Text.DoesNotEndWith(string expected) //RegexConstraint,是不是符合正则 RegexConstraint(string pattern) Is.StringMatching(string pattern) Matches(string pattern) [Obsolete] Text.Matches(string pattern) [Obsolete] Text.DoesNotMatch(string pattern)
其中,Text类的所有方法从2.5.1中开始推荐不使用,将在3.0后移除。
细节:
ContainsSubstring、Contains、Matches、StartsWith和EndsWith 只能出现在约束表达式内部或者子类中。
Expect( "Make your test", StartsWith( "Make" ) );
Contains 不只是一个字符串的约束但是当参数为字符串时可以转化为字符串约束。
8,Collection Constraints (NUnit 2.4 / 2.5)
集合约束,就是用来测试集合的,以下约束在2.4.6之前只支持集合,但是之后支持集体继承IEnumerable的所有对象。从2.4.2开始,传入一个不正确类型的参数将会产生一个错误而不是以前的版本的测试失败。
//AllItemsConstraint,测试集合中所有的成员是否满足约束条件,只有都满足测试成功 // 构造函数 AllItemsConstraint(Constraint itemConstraint) //语法调用 Is.All... Has.All... //示例 int[] iarray = new int[] { 1, 2, 3 }; string[] sarray = new string[] { "a", "b", "c" }; Assert.That( iarray, Is.All.Not.Null ); Assert.That( sarray, Is.All.InstanceOf() ); Assert.That( iarray, Is.All.GreaterThan(0) ); Assert.That( iarray, Has.All.GreaterThan(0) ); //SomeItemsConstraint,测试集合的所有成员是否满足约束条件,只要有一个满足就测试成功 // 构造函数 SomeItemsConstraint(Constraint itemConstraint) //语法调用 Has.Some... //NoItemConstraint,测试集合的成员是否满足约束条件,都不满足时测试成功 //构造 NoItemConstraint(Constraint itemConstraint) //语法调用 Has.None... Has.No... //UniqueItemsConstraint,测试集合的每个项是不是独一无二的 // 构造 UniqueItemsConstraint() //语法调用 Is.Unique //CollectionContainsConstraint,测试一个集合是否包含传入的项 // 构造 CollectionContainsConstraint( object ) //语法 Has.Member( object ) Contains.Item( object ) // 修改器 ...Using(IComparer comparer) ...Using(IComparer<T> comparer) ...Using(Comparison<T> comparer) //CollectionEquivalentConstraint,测试两个集合是否相等:包含同样的项,不考虑顺序 // 构造 CollectionEquivalentConstraint( IEnumerable other ) //语法 Is.EquivalentTo( IEnumerable other ) //CollectionSubsetConstraint,测试一个集合是不是另一个的子集 //构造 CollectionSubsetConstraint( ICollection ) // 语法 Is.SubsetOf( IEnumerable ) // CollectionOrderedConstraint (NUnit 2.5),测试一个集合是不是有序的 //构造 CollectionOrderedConstraint() //语法 Is.Ordered //修改器 ...Descending ...By(string propertyName) ...Using(IComparer comparer) ...Using(IComparer<T> comparer) ...Using(Comparison<T> comparer)
细节:
1,Has.Member 会用对象的相等性来测试是否集合的成员。如果测试是否和集合中的某个项相等,请用Has.Some.EqualTo(...).
2,修改器可能会应用多个,可以以任意顺序。如果同样的修改器在一个表达式中应用多次,结果是不可预期的。
9,Property Constraints (NUnit 2.4.2)
//PropertyExistsConstraint,测试一个对象是否包含这个属性 // 构造 PropertyExistsConstraint(string name) //语法 Has.Property( string ) //PropertyConstraint,测试对象是否包含这个属性,并进而去测试这个属性的值 //构造 PropertyConstraint(string name) //语法 Has.Property(string)... //示例 Assert.That(someObject, Has.Property("Version").EqualTo("2.0")); Assert.That(collection, Has.Property("Count").GreaterThan(10));
还有一些其它的属性约束可以做为其它的约束的前缀来应用到属性上。
Has.Length... Has.Count... Has.Message... Has.InnerException...
10,Throws Constraint (NUnit 2.5)
ThrowsConstraint 是用来测试以委托形式出现的代码会不会抛出一个特定的异常。它可以单独来使用,也可以和其它约束来限定要抛出的异常。而ThrowsNothingConstraint 只是用来断言这个委托不会抛出任何异常。
// 构造 ThrowsConstraint(Type expectedType) ThrowsConstraint<T>() ThrowsConstraint(Type expectedType, Constraint constraint) ThrowsConstraint<T>(Constraint constraint) ThrowsNothingConstraint() // 语法 Throws.Exception Throws.TargetInvocationException Throws.ArgumentException Throws.InvalidOperationException Throws.TypeOf(Type expectedType) Throws.TypeOf<T>() Throws.InstanceOf(Type expectedType) Throws.InstanceOf<T>() Throws.Nothing Throws.InnerException //示例 // .NET 1.1 Assert.That( new TestDelegate(SomeMethod), Throws.TypeOf(typeof(ArgumentException))); Assert.That( new TestDelegate(SomeMethod), Throws.Exception.TypeOf(typeof(ArgumentException))); Assert.That( new TestDelegate(SomeMethod), Throws.TypeOf(typeof(ArgumentException)) .With.Property("Parameter").EqualTo("myParam")); Assert.That( new TestDelegate(SomeMethod), Throws.ArgumentException ); Assert.That( new TestDelegate(SomeMethod), Throws.TargetInvocationException .With.InnerException.TypeOf(ArgumentException)); // .NET 2.0 Assert.That( SomeMethod, Throws.TypeOf<ArgumentException>()); Assert.That( SomeMethod, Throws.Exception.TypeOf<ArgumentException>()); Assert.That( SomeMethod, Throws.TypeOf<ArgumentException>() .With.Property("Parameter").EqualTo("myParam")); Assert.That( SomeMethod, Throws.ArgumentException ); Assert.That( SomeMethod, Throws.TargetInvocationException .With.InnerException.TypeOf<ArgumentException>());
细节:
1,Throws.Exception 之后可以跟其它的约束,就像上面的最后两个例子,它们会被应用到异常本身。它也可以单独来使用,用来检查会抛出异常,而不指出其特定的类型。这在实际中是不推荐的,因为你应该知道什么类型的异常会被抛出。
2,Throws.TypeOf 和 Throws.InstanceOf 是这类测试的一个简单语法调用。它们和被应用在Throws.Exception之后效果是一样的。
3,Throws.TargetInvocationException, Throws.ArgumentException 和 Throws.InvalidOperationException 提供了一些常用异常的简单调用方式。
4,Throws.InnerException 单独使用时是用来测试抛出异常的内部异常。更多的时候,是和外部异常一起使用的。
11,Compound Constraints (NUnit 2.4)
这些主要是用来连接其它的约束。
//构造 NotConstraint( Constraint ) // 取消或反转约束 AllItemsConstraint( Constraint ) // 测试集合的所有成员是否满足约束 AndConstraint( Constraint, Constraint ) //测试两种约束都要满足 OrConstraint( Constraint, Constraint ) // 测试两个约束至少一个 // 语法 Is.Not... Is.All... Constraint & Constraint Constraint | Constraint // 示例 Assert.That( 2 + 2, Is.Not.EqualTo( 5 ); Assert.That( new int[] { 1, 2, 3 }, Is.All.GreaterThan( 0 ) ); Assert.That( 2.3, Is.GreaterThan( 2.0 ) & Is.LessThan( 3.0 ) ); Assert.That( 3, Is.LessThan( 5 ) | Is.GreaterThan( 10 ) );
12,Delayed Constraint (NUnit 2.5)
DelayedConstraint 会推迟别一个约束的应用直到设定的时间过去。它替代了代码中的Sleep,但是它也支持轮询:能让等待更长的时间,但是同时也能保持测试尽快的运行。After 修改器可以应用到任何约束上,这个延迟会应用到直到 After出现的以前的整个约束表达式。
//构造 DelayedConstraint(Constraint, int) // 一个延迟之后再测试约束 DelayedConstraint(Constraint, int, int) // 用轮询,延迟之后再测试约束 // 语法 After(int) After(int, int)
13,List Mapper (NUnit 2.4.2)
和其它约束不一样,ListMapper是用来修改Assert.That()的真实值参数:它要求真实值必须是集合,转化为别一个新的集合去测试是否满足约束。如今,ListMapper 支持一种转化:用属性值创建一个新的集合。
通常ListMapper可以用List.Map() 语法帮助类或等效的 Map().
下面的例子是同一个断言的三种不同方式:
string[] strings = new string[] { "a", "ab", "abc" }; int[] lengths = new int[] { 1, 2, 3 }; Assert.That(List.Map(strings).Property("Length"), Is.EqualTo(lengths)); Assert.That(new ListMapper(strings).Property("Length"), Is.EqualTo(lengths)); // Assuming inheritance from AssertionHelper Expect(Map(strings).Property("Length"), EqualTo(lengths));
14,ReusableConstraint (NUnit 2.5.6)
有些时候复用约束可能导致意想不到的结果。看下面的例子:
// 一般情况下的调用 Constraint myConstraint = Is.Not.Null; Assert.That("not a null", myConstraint); // 通过 Assert.That("not a null", myConstraint); // 失败,为何? //现在我们用本节提供的约束 ReusableConstraint myConstraint = Is.Not.Null; Assert.That("not a null", myConstraint); // 通过 Assert.That("not a null", myConstraint); // 通过 // 或者 var myConstraint = new ReusableConstraint(Is.Not.Null); Assert.That("not a null", myConstraint); // 通过 Assert.That("not a null", myConstraint); // 通过
原因:
在之前的例子中,myConstraint 的值是一个没有解析的约束。事实上,这是一个未解析的NullConstraint,因为这个表达式最后的一个约束是Null。它之前的Not还没有被应用。但是Assert.That()知道在使用之前如何解析这个表达式:它把这个约束解析成一个NotConstraint 而不是表面上的NullConstraint。当然,之前myConstraint 在离开之前并没有改变。但是,相等性约束已经被解析了,这个表达式是一个解析过的约束,就不需要被第二个Assert.That()再次解析,它只是认为它是一个NullConstraint 而不是NotConstraint。
因此,为了复用,我们要保存这个约束的解析的结果,在这个例子中就是
NotConstraint => NullConstraint
这就是ReusableConstraint的作用,它解析了整个表达式和保存了解析的结果,执行时就可以直接运用结果。
你可以在任何需要复用的场合使用它,当然你也可以在以下的场合中不使用:
1,只是一个简单的约束,没有任何的附加操作
2,当使用到约束时,都用构造函数方式,而不是语法的”.”。
但是,在以下场合使用ReusableConstraint 也不会有任何重大的负作用,还能让代码更加清晰易懂并且在以后发布的版本不会出现异常。
约束介绍完毕,下文将介绍特性attribute和可扩展性Extensibility。