NSubstitute完全手册(十五)自动递归模拟
替代实例一旦被设置属性或方法,则将自动返回非NULL值。例如,任何属性或方法如果返回接口、委托或纯虚类*,则将自动的返回替代实例自身。通常这被称为递归模拟技术,而且是非常实用的。比如其可以避免显式地设置每个替代实例,也就意味着更少量的代码。诸如String和Array等类型,默认会返回空值而不是NULL。
*注:一个纯虚类是指一个类,其所有的公有方法和属性都被定义为virtual或abstract,并且其具有一个默认公有或受保护地无参构造函数。
递归模拟
比如说我们有如下类型定义:
1 public interface INumberParser 2 { 3 int[] Parse(string expression); 4 } 5 public interface INumberParserFactory 6 { 7 INumberParser Create(char delimiter); 8 }
我们想配置 INumberParserFactory 来创建一个解析器,该解析器会为一个 expression 返回一定数量的 int 类型的值。我们可以手工创建每个替代实例:
1 [TestMethod] 2 public void Test_AutoRecursiveMocks_ManuallyCreateSubstitutes() 3 { 4 var factory = Substitute.For<INumberParserFactory>(); 5 var parser = Substitute.For<INumberParser>(); 6 factory.Create(',').Returns(parser); 7 parser.Parse("an expression").Returns(new int[] { 1, 2, 3 }); 8 9 var actual = factory.Create(',').Parse("an expression"); 10 CollectionAssert.AreEqual(new int[] { 1, 2, 3 }, actual); 11 }
或者可以应用递归模拟功能,INumberParserFactory.Create() 会自动返回 INumberParser 类型的替代实例。
1 [TestMethod] 2 public void Test_AutoRecursiveMocks_AutomaticallyCreateSubstitutes() 3 { 4 var factory = Substitute.For<INumberParserFactory>(); 5 factory.Create(',').Parse("an expression").Returns(new int[] { 1, 2, 3 }); 6 7 var actual = factory.Create(',').Parse("an expression"); 8 CollectionAssert.AreEqual(new int[] { 1, 2, 3 }, actual); 9 }
每次当使用相同参数调用一个被递归模拟的属性或方法时,都会返回相同的替代实例。如果使用不同参数调用,则将会返回一个新的替代实例。
1 [TestMethod] 2 public void Test_AutoRecursiveMocks_CallRecursivelySubbed() 3 { 4 var factory = Substitute.For<INumberParserFactory>(); 5 factory.Create(',').Parse("an expression").Returns(new int[] { 1, 2, 3 }); 6 7 var firstCall = factory.Create(','); 8 var secondCall = factory.Create(','); 9 var thirdCallWithDiffArg = factory.Create('x'); 10 11 Assert.AreSame(firstCall, secondCall); 12 Assert.AreNotSame(firstCall, thirdCallWithDiffArg); 13 }
注:不会为类创建递归的替代实例,因为创建和使用类可能有潜在的或多余的副作用。因此,有必要显式地创建和返回类的替代实例。
替代链
当需要时,我们可以使用递归模拟来简单地设置替代链,但这并不是一个理想的做法。例如:
1 public interface IContext 2 { 3 IRequest CurrentRequest { get; } 4 } 5 public interface IRequest 6 { 7 IIdentity Identity { get; } 8 IIdentity NewIdentity(string name); 9 } 10 public interface IIdentity 11 { 12 string Name { get; } 13 string[] Roles(); 14 }
如果要获取 CurrentRequest 中的 Identity 并返回一个名字,我们可以手工为 IContext、IRequest 和 IIdentity 创建替代品,然后使用 Returns() 将这些替代实例链接到一起。或者我们可以使用为属性和方法自动创建的替代实例。
1 [TestMethod] 2 public void Test_AutoRecursiveMocks_SubstituteChains() 3 { 4 var context = Substitute.For<IContext>(); 5 context.CurrentRequest.Identity.Name.Returns("My pet fish Eric"); 6 Assert.AreEqual( 7 "My pet fish Eric", 8 context.CurrentRequest.Identity.Name); 9 }
在这里 CurrentReques t是自动返回一个 IRequest 的替代实例,IRequest 替代实例会自动返回一个 IIdentity 替代实例。
注:类似于这种设置很长的替代实例链,一般被认为是代码臭味:我们打破了 Law of Demeter 原则,对象只应该与其直接关系的临近对象打交道,而不与临近对象的临近对象打交道。如果你写的测试用例中没有使用递归模拟,设置的过程可能会明显的变复杂,所以如果要使用递归模式,则需要格外的注意类似的类型耦合。
自动值
当属性或方法返回 String 或 Array 类型的值时,默认会返回空或者非 NULL 值。比如在你仅需要返回一个对象引用,但并不关心其特定的属性时,这个功能可以帮你避免空引用异常。
1 [TestMethod] 2 public void Test_AutoRecursiveMocks_AutoValues() 3 { 4 var identity = Substitute.For<IIdentity>(); 5 Assert.AreEqual(string.Empty, identity.Name); 6 Assert.AreEqual(0, identity.Roles().Length); 7 }