使用 PostSharp 模拟方法和属性





4.00/5 (1投票)
使用 PostSharp 无需接口,模拟静态方法和属性
引言
首先,背景介绍。引用维基百科的话:“在面向对象编程中,模拟对象是模拟对象的仿真对象,它们以可控的方式模仿真实对象的行为。程序员通常会创建一个模拟对象来测试其他对象的行为”。它是测试驱动开发中一种重要的技术。 然而,要使用这项技术,对象必须是“可模拟的”。在 C# 中,这通常意味着带有接口或抽象的实例对象,例如 NMock 中使用的。静态对象和静态方法通常是不可模拟的。
借助 PostSharp,模拟任何有值的东西都成为可能,这包括返回值的方法以及实例或静态的属性。即使对象本身不是字面上“模拟”的,如果它的所有或部分属性和方法都被模拟,那么它就等同于被模拟了。
对于不太熟悉 PostSharp 的人来说,它是一种可以拦截任何方法或属性的使用的工具。在不实际调用被拦截的方法或属性的情况下,可以返回新值。PostSharp 提供了更多功能,这是他们网站的链接:http://www.sharpcrafters.com/。
使用代码
要使用文章附带的小型框架,典型的测试步骤如下:
- 将 MockMethod 属性附加到目标方法,将 MockProperty 附加到目标属性。
- 通过添加拦截条件和拦截时的返回值来设置 TestApparatus。
- 编写涉及这些被模拟方法和属性的测试,然后运行。
以下是一个非常简单的测试:
1 public class StaticProvider
2 {
3 [MockMethod]
4 public static bool Provide(int input)
5 {
6 throw new NotImplementedException("Static Provider Not Implemented");
7 }
第 3 行将第 4 行的 Provide 方法标记为可模拟方法。
1 [TestMethod]
2 [Description("With both methods mocked, the test should pass")]
3 public void AllSuccess()
4 {
5 TestApparatus.MockStatic<bool, int>(StaticProvider.Provide).Any().Return(true);
6 TestApparatus.MockInstance<InstancedProvider, bool, int>(i => i.Provide).Expecting(1).If(a1 => a1 >= 0).Return(true);
7 SampleObject sample = new SampleObject();
8 sample.Methods();
9 }
第 5 行告诉框架拦截对静态方法 StaticProvider.Provide(int) 的任何调用,并返回 true。
第 6 行让框架在输入值大于 0 时拦截实例方法 InstancedProvider.Provide(int),然后返回 true。它还告诉框架在测试期间只期望一次调用。
以下示例显示了如何模拟属性。
1 [TestMethod]
2 [Description("Mocking a specific property")]
3 public void MockingProperty()
4 {
5 const int mocked = 12034;
6 TestApparatus.MockProperty(() => StaticProvider.Value).Any().Return(mocked);
7 TestApparatus.MockProperty((InstancedProvider i) => i.Value).Expecting(1).Any().Return(mocked);
8 SampleObject sample = new SampleObject();
9 Assert.AreEqual(mocked, sample.Values());
10 }
第 6 行模拟静态属性 StaticProvider.Value,以便随时返回模拟值。
第 7 行模拟实例属性 InstancedProvider.Value,以便随时运行模拟值。
框架内部发生的事情相当直接。所有被模拟的方法和属性都会被内部拦截,但只有那些符合指定条件的才会被覆盖。该框架所做的就是记录拦截的要求并在运行时执行它们。
该项目是使用 Visual Studio 2008 和 .Net 3.5 完成的。已验证该项目在 Visual Studio 2012 下无需任何更改即可运行。必须按顺序安装 PostSharp,并且可能需要刷新 PostSharp 库的引用。
关注点
这种基于 PostSharp 的模拟方法需要被模拟的方法和属性的源代码。代码在编译期间被 PostSharp 透明地修改。也就是说,二进制文件与未编译 PostSharp 的二进制文件不同。使用 Reflector 或 ILDASM 可以发现这些差异。好消息是,得益于 PostSharp,修改后的二进制文件运行起来与未修改的版本相同,并且更改的逻辑对调试器是隐藏的。那么,自然地,人们会问如何模拟没有源代码的系统方法,例如 int.Parse()。一种解决方案是围绕系统方法编写一个包装方法,并使新方法可模拟。将所有调用重定向到新方法而不是原始方法。同时围绕新方法而不是原始方法设置测试。下面显示了一个示例。
1 [MockMethod]
2 public int Parse(string s)
3 {
4 return int.Parse(s);
5 }
6 [TestMethod]
7 public void Test()
8 {
9 TestApparatus.MockInstance<Tests, int, string>(t => t.Parse).Any().Return(0);
10 }
这种修改二进制文件的方法的另一个问题是,被测试的二进制文件不是要部署的二进制文件。它们彼此等价,正如 PostSharp 所保证的。因此,顾虑或多或少是不要将测试版本部署到生产环境中。在提出的框架中,MockMethodAttribute 和 MockPropertyAttribute 包含条件语句,以便在非调试构建中将它们更改为简单的属性。PostSharp 将不会识别这些简单的属性,因此将保持生产二进制文件不变。这有助于区分测试二进制文件和生产二进制文件。
总之,与基于接口的模拟测试框架(如 NMock 和 Rhino Mock)相比,这种方法可以自由地模拟任何静态内容。测试目的无需接口,这对于最初未针对模拟测试设计的代码来说是一种解脱。与基于性能分析器的模拟测试框架(如 JustMock)相比,这种方法在性能方面具有优势,因为只有带有属性的方法和属性才会触发拦截。
结语
这个发布的示例只是一个小型演示项目,用于展示如何进行模拟。但是,对于任何有兴趣的人来说,它包含了足够的信息来构建一个完整的测试框架。一些有用的补充可能包括:拦截对象创建、在拦截时执行自定义逻辑而不是返回简单值。
感谢阅读。