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     }

NSubstitute 完全手册

posted @ 2013-05-22 12:50  sangmado  阅读(1733)  评论(0编辑  收藏  举报