DSL:深入(ish)研究






4.99/5 (76投票s)
探讨常见的内部DSL技术,并提供一个模拟框架的示例
目录
- 引言
- 语义模型
- 构建器
- 解析树
- 方法链
- 流畅接口
- 知道何时结束
- 渐进式接口
- 简单示例
- 通用示例
- 就是这样
引言
距离我上次在CodeProject写文章已经有一段时间了,但这并不意味着我没有忙碌。远非如此,我一直在忙于消化新事物(至少对我来说是新的),并阅读了很多。事实上,我刚读完的书(Martin Fowler的《领域特定语言》)在某种程度上启发了这篇文章。
我一直对软件工程中更晦涩的元素感兴趣,当然我喜欢用WPF做一些漂亮的东西,但最近我发现自己越来越多地回到我的计算机科学根源,并渴望探索一些更奇怪的领域,让我告诉你,领域特定语言(DSL)是一个相当奇怪(但强大)的地方。
如果有人要遵循Martin Fowler的观点,DSL世界将分为两部分
- 内部DSL:这些DSL旨在供软件工程师/开发人员使用,不应向最终用户展示。因此,它们可能包含相当技术性的语法。
- 外部DSL:这些DSL旨在供最终用户使用,因此可以预期它们以最终用户和必须解析DSL文本的软件工程师都能理解的通用语言进行描述。
现在让我告诉你,Martin Fowler的《领域特定语言》这本书有大约600页长,并且深入到了我甚至无法尝试将其浓缩成一篇文章的细节。所以在这篇文章中我们根本不会涉及外部DSL,而是将重点关注内部DSL工具/技巧的一个子集。
我将从回顾开发内部DSL的几个常见想法/技术开始,然后通过一个简单的例子,最后我们将继续处理通用示例。
语义模型
简单来说,这就是由DSL填充的模型。DSL所做的就是提供一种可读的方式来填充该模型。
Martin Fowler在关于他书中关于状态机DSL的语义模型方面有一些很好的说法。
从这个角度来看,DSL仅仅是表达模型如何配置的机制。使用这种方法的大部分好处来自于模型而不是DSL。我能够轻松地为客户配置一个新的状态机,这是模型的一个属性,而不是DSL。我可以在运行时更改控制器而无需编译,这是模型的一个特性,而不是DSL。我在多个控制器安装中重用代码,这是模型的一个属性,而不是DSL。因此,DSL仅仅是模型的一个薄外壳。
http://www.informit.com/articles/article.aspx?p=1592379&seqNum=4
构建器
通过使用单独的类来构建我们的语义模型,我们可以使我们的语义模型保持干净,没有任何DSL解析代码,这说明了解析代码和给定DSL所代表的最终语义模型之间存在良好的分离。
我在这里不会过多地谈论构建器,因为它们更容易通过例子来理解,它们实际上只是一些辅助类,它们能够创建填充正确的语义模型。正如我所说,你将在本文相关的示例中看到演示。
解析树
解析树是在创建内部DSL时可以使用的一个有趣的东西。那么解析树到底是什么呢?嗯,很容易掌握这些东西的方法就是考虑将代码树作为数据结构传递的能力。
解析树是一个相当新颖的东西,并非所有语言都支持此概念。.NET幸运地拥有Expression命名空间,可用于此目的。为了完整起见,Expression类被引入,以允许LINQ to SQL/EF和IQueryProvider
将作为数据结构传递的代码转换为SQL语句,但这与我们的主题无关,重要的是.NET支持将代码作为数据结构传递的能力,这允许我们将强类型代码像普通数据类型一样传递。
最常用/常见/流行的例子可能是从LambdaExpression
中提取属性名称,如下所示:
public static PropertyInfo GetProperty<T>(Expression<Func<T, Object>> propertyExpression)
{
var lambda = propertyExpression as LambdaExpression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambda.Body as MemberExpression;
}
var propertyInfo = memberExpression.Member as PropertyInfo;
return propertyInfo;
}
我们可能会这样使用它,这通常与实现INotifyPropertyChanged
的对象一起使用。
PropertyInfo prop = <GetProperty<Person>(x => x.Age)<
方法链
方法链是一种常用的技术,其中每个方法都返回一个对象(在大多数情况下,它本身就是当前对象或构建特定语义模型部分的构建器对象),这允许将调用链接成一个语句。
流畅接口
我在Ayende的博客上找到了一篇很好的文章(他也有自己的DSL书籍,所以你可以信任他,别担心),它描述了我认为的流畅接口的本质以及它们如何与方法链进行比较。这是Ayende所说的:
方法链当然是你会在流畅接口中使用的东西,但这就像说在构建插件框架时需要使用接口一样。你正在使用某样东西的事实并不意味着你所做的事情只是那个东西。
流畅接口与简单的方法链不同,因为它们允许你用领域术语表达你的意图,并允许你获得更具可读性的代码。方法链、运算符重载、糟糕的泛型技巧当然都是其中的一部分,但最终结果远不止是一个简单的函数链。
Ayende还展示了方法链和流畅接口之间的精彩比较,这是他的博客所说的:
这是方法链
string user = new StringBuilder()
.Append("Name: ")
.Append(user.Name)
.AppendLine()
.Append("Email: ")
.Append(user.Email)
.AppendLine()
.ToString();
这是流畅接口
return new Finder<Order>(
Where.Order.User == CurrentUser &&
(
Where.Order.TotalCost > Money.Dollars(150) ||
Where.Order.OrderLines.Count > 15
),
OrderBy.Order.CreatedAt
).List();
我强烈建议你阅读Ayende的博客以获取完整细节:http://ayende.com/blog/2639/fluent-interfaces-method-chaining。
知道何时结束
在使用方法链时,一个主要问题是如何保持正确的调用顺序以及如何知道链何时真正完成。那么,一个人怎么知道何时完成链,从而返回一个填充的语义模型?
处理这个困境最简单的方法可能是包含某种“结束”方法,该方法表示链的结束。
渐进式接口
在使用方法链时,另一个问题是你可能总是返回当前对象,这可能允许用户调用过多的方法/错误的方法,在错误的地方。一个常见的处理技术是返回接口,这些接口规定在链的那个点上哪些方法/属性等是有效的。这种技术被称为渐进式接口(至少Martin Fowler喜欢这样称呼它)。
通过使用渐进式接口,调用代码可以调用的方法链将被限制在链在该点上由接口公开的方法。
简单示例
第一个简单的例子非常容易,应该会展示我们刚刚讨论的几个技术,例如,您将看到方法链/流畅接口/构建器/语义模型的示例,并且我们将知道何时结束链。
场景
所以在我们看演示代码之前,让我们先设定一下我们正在做什么的场景。我曾经热衷于创作电子音乐,我的工作室里摆满了各种电子设备,所以我认为一个创建简单音乐工作室的DSL可能会很有趣。工作室很简单,只有两样东西:
- 一个混音台
- 任意数量的采样器
简单示例:语义模型
就语义模型而言,这就是我们将要创建的。我可以进一步解释,但我认为我们大多数人都能/应该理解类图。
简单示例:构建器
正如前面所说,通过使用辅助构建器类,我们可以将DSL构建/解析代码与语义模型分开,这是一个好现象。
StudioBuilder
对于这个场景,我们需要首先关注的是顶层构建器,也就是将为我们构建新Studio
对象的构建器。下面展示了这一点。
public class StudioBuilder
{
private MixingDeskBuilder currentMixingDesk;
private SamplerBuilder currentSampler;
private List<Sampler> loadedSamplers = new List<Sampler>();
public static StudioBuilder Studio()
{
return new StudioBuilder();
}
public StudioBuilder MixingDesk()
{
currentMixingDesk = new MixingDeskBuilder();
return this;
}
public StudioBuilder Channels(int channels)
{
currentMixingDesk.Channels = channels;
return this;
}
public SamplerBuilder Sampler()
{
if (currentSampler != null)
loadedSamplers.Add(currentSampler.GetValue());
currentSampler = new SamplerBuilder(this);
return currentSampler;
}
public Studio End()
{
return GetValue();
}
private Studio GetValue()
{
return new Studio(currentMixingDesk.GetValue(), Samplers);
}
private List<Sampler> Samplers
{
get
{
List<Sampler> samplers = new List<Sampler>();
samplers.AddRange(loadedSamplers);
if (currentSampler != null)
samplers.Add(currentSampler.GetValue());
return samplers;
}
}
}
这里有几点需要注意,例如:
- 使用的上下文变量,用于存储DSL在构建时的当前上下文。这可以通过
currentMixingDesk
和currentSampler
变量看到。这些变量确保我们在使用DSL创建时正在操作正确的对象。 - 方法链的使用,例如
MixingDesk()
方法。 - 它确实展现了一个流畅的接口,即方法名已被选择来向用户表达其意图,以帮助构建DSL。
- 有一种方法可以指示链的结束,例如
End()
方法。
MixingDeskBuilder
现在我们有了顶层构建器,让我们稍微关注一下MixingDeskBuilder
。请记住,在此演示DSL中只允许有一个,因此此构建器的构建非常简单。这是它:
public sealed class MixingDeskBuilder
{
private const int DEFAULT_CHANNELS = 10;
private int channels = DEFAULT_CHANNELS;
public int Channels
{
get { return channels; }
set { channels = value; }
}
public MixingDesk GetValue()
{
return new MixingDesk(channels);
}
}
说实话,这个没什么好说的。
SamplerBuilder
接下来我们继续看SamplerBuilder
。请记住,在此演示DSL中允许多个,因此构建器需要一种方法来允许创建多个。实现这一目标的方法是获取父级StudioBuilder
的引用,并拥有一个Sampler()
方法,该方法简单地调用父级的Sampler()
方法,该方法将一个新的SamplerBuilder
添加到其集合中。
public class SamplerBuilder
{
private StudioBuilder parent;
private int hardDiskSize;
private string model;
public SamplerBuilder(StudioBuilder parent)
{
this.parent = parent;
}
public SamplerBuilder DiskSize(int hardDiskSize)
{
this.hardDiskSize = hardDiskSize;
return this;
}
public SamplerBuilder Model(string model)
{
this.model = model;
return this;
}
public SamplerBuilder Sampler()
{
return parent.Sampler();
}
public Studio End()
{
return parent.End();
}
public Sampler GetValue()
{
return new Sampler(hardDiskSize, model);
}
}
简单示例:用法
好的,所以要使用这个简单的演示示例,我们只需这样做:
Studio studio = StudioBuilder.Studio()
.MixingDesk()
.Channels(10)
.Sampler()
.DiskSize(1000)
.Model("Akai 3000")
.Sampler()
.DiskSize(1000)
.Model("Emu ultra")
.End();
运行后将产生类似这样的结果:
通用示例
在我们开始这一部分之前,我还需要提供更多关于这篇文章如何产生的背景信息。在我大致读完Martin Fowler的《领域特定语言》这本书的时候,我还在调试我工作中的一些代码,当时我正在进行一个测试用例,我使用了很棒的Moq库,这是我最喜欢的模拟框架,这时发生了两件事:
- 我使用我的mock设置的某个回调出现问题,它不起作用,我开始调试我的代码,我注意到了一件我立刻认出是Castle动态代理的东西。我突然对Moq有了顿悟,就像,啊哈,他们就是这么做的。事实是他们在主页上提到了这一点,但我从未读过那个页面,因为Moq从来没有真正出过问题,所以我就没有看过。
- 我看了Moq的配置,发现它做了很多Martin Fowler的《领域特定语言》书中提到的事情。例如,Moq使用了以下DSL技术:
- 方法链
- 流畅接口
- 渐进式接口
- 解析树
我向Moq团队致以崇高的敬意,我还说,多么及时啊!真正的时机,你说什么。
所以,事不宜迟,我想知道我是否可以从头开始,利用我新发现的DSL知识来编写类似Moq的东西。我花了一段时间,但我已经设法制作了一个可用的Moq克隆。我应该指出,我这样做完全没有参考他们的代码,我相信如果你比较两者,你会明白的。现在我完成了,我显然查看了他们的代码,他们的代码就像,嗯,嗯,简直比我的好。
现在,在任何人提到令人讨厌的“抄袭”一词之前,我实际上已经联系了Moq的主要作者 Kzu,在他写这篇文章之前很久就告诉了他我正在做什么,他给了我许可。如果有人感兴趣,这是我们的电子邮件链。(点击下面的图片查看大图)
所以,你看,我得到了Kzu的祝福。
我还有一件事需要指出,那就是我的简单
Moq克隆在这里只是一个克隆,它很简单,远没有Moq那样精致,所以任何想用我的简单演示来满足他们模拟需求的人,算了吧,它只是为了好玩而已,请使用Moq。
这并不是说我对我的成果不满意,我实际上挺满意的,因为它实现了Moq在DSL功能方面的大部分主要功能,这毕竟是我尝试编写这个简单的Moq克隆的主要原因。所以,是的,我对这一切如何发展感到满意。
为了完整起见,以下是一些你为什么会使用Moq而不是我简单的克隆的原因,正如我所说,这更多是为了尝试创建Moq所使用的DSL/方法链/流畅接口/渐进式接口/解析树语法。
总之,我们偏离主题了,以下是你为什么需要使用Moq而不是我的简单示例:
- 我只支持模拟接口
- 我不支持模拟类
- 我不支持嵌套模拟
- 我不处理
ref
变量
总之,现在我们已经完成了所有这些技术性废话,让我们继续进行第二个示例,这是我的简单Moq克隆,我称之为“通用示例”。
我还要指出,尽管我的简单Moq克隆比Moq简单,但它确实有效,你可以通过运行本文附带的演示代码来看到这一点。
通用示例:总体思路
正如我刚所说,我创建了一个非常简单的模拟框架,它使用内部DSL来构建一个模拟对象,该对象可以替代真实对象。我也说过我的简单DSL只支持一部分正常模拟框架产生的功能,但我不在乎,我在乎的是向大家展示我如何做到这一点的所有技术以及你可以用在你自己的DSL中的一些类似DSL的语法。
那么,既然我们知道我们正在创建一个DSL来创建一个模拟框架,我们可以看到哪些东西?
好吧,如果我们考虑一个典型对象允许什么,我们可以设想一个初步的列表如下:
- 模拟方法
- 模拟属性
- 引发事件
- 抛出异常
所以这些是我们可能想要包含在我们的DSL/语义模型中的东西。然而,当我们更详细地研究这些领域时,我们可以开始考虑正常(即非模拟对象)对象运行的方式。它可能允许各种行为,例如:
- 在属性设置器中抛出错误
- 只接受某些输入
- 期望只调用一定的次数
你明白了,所以考虑到这些,我们可以开发我们的DSL。我选择模仿我认为一个非常著名且聪明的现有API(即Moq),但你可以看到这些额外的考虑如何影响你的DSL设计。
通用示例:语义模型
在某些方面,这个语义模型非常简单,它归结为:
- 方法
- 属性
这实际上是我们使用DSL所能创建的全部。使语义模型更有趣的是,方法可能需要做诸如抛出Exception
或引发事件的事情。因此,这些考虑和处理这些问题的能力就成为语义模型的一部分。
通用示例:常用技术
本节将概述“通用示例”中使用的常用技术,这些技术将为后续章节提供构建块。
常用技术:代理
由于这个DSL都是关于模拟的,因此有一个内部代理对象也就不足为奇了,该代理对象被用作真实对象的替代品。你可能会问,这个代理是如何产生的?谁创建了它?
有很多免费的开源框架可以处理动态创建的代理,事实上,使用.NET Remoting API来创建简单的代理并不难。话虽如此,我选择了使用Castle Windsor,这是一个经过考验和测试的工具,你可以说。
所以,是的,我们将使用Castle Windsor代理来生成此通用示例DSL中的模拟对象。
常用技术:拦截
要做到“通用示例”正在做的事情,我们显然进入了一个相当小众的领域,我们期望当我们做X时,会从方法中返回某些东西,或者我们期望当属性Y被设置时,其值将是Z。这都相当具体于真实对象的工作方式,但我们不想编写真实对象来处理我们的愿望,我们想做的是编写一些DSL代码来指定行为,例如:
当调用方法X时返回List<Foo>
那么我们该如何着手呢?嗯,幸运的是,市面上有许多框架允许我们这样做。说实话,这只是面向切面编程(AOP),那么AOP是如何工作的呢?嗯,在.NET领域,它通常通过方法拦截来工作,但我们如何做到这一点?
嗯,就像我说的那样,有许多免费的开源框架可以用来处理.NET的AOP。对于本文,我选择了使用Castle Windsor,它使用一种称为拦截的技术,而这通过使用Interceptors来实现。
简单来说,Interceptors允许你挂接到方法调用管道,并决定(如果适用)将什么返回值/参数传递给基本方法实现。
这是一种非常强大的技术。通过使用Interceptors,我们几乎可以处理任何模拟需求。在一个Interceptor中,我们可以做到以下几点:
- 在方法被调用时执行回调
- 增加一个计数器,记录方法被调用的次数
- 判断方法调用是否有效
- 在特定的方法/属性调用期间,如果被要求,抛出
Exception
- 在特定的方法/属性调用期间,引发事件
这项技术在整个“通用示例”中都有使用,其中使用了以下Interceptors:
常用技术:查找事件名称
实际引发事件是一个棘手的难题,也是我为本文所做的所有工作中最具挑战性的方面之一。你可能会问,为什么引发事件的代码如此困难?这是因为我希望能够以一种强类型的方式标识要引发的事件,也就是说,我不想传递魔术字符串。
这才是真正的问题,但为什么这是一个问题?
嗯,在.NET中,对于使用事件的外部类,你唯一能做的就是使用+=
或-=
运算符添加或删除处理程序。那么,仅仅通过使用这些运算符,我们如何获得实际的事件名称呢?
这是一个相当困难的问题,而且我并没有立刻想到答案。下图说明了我最终想出的办法。
用文字来说,我们接受一个接受T
的委托,而T
恰好是我们正在构建的模拟对象的类型。所以我希望这部分是没问题的。接下来,我们创建了一个极其短暂的Castle动态代理,类型为T
,它仅用于分析通过Castle的拦截功能对它进行的调用,而Castle的拦截功能仅用于捕获关于代理上调用了哪些方法/事件等的信息。我称之为PredictiveInterceptor
。
所以我们现在有一个代理对象和一个PredictiveInterceptor
,它将存储在代理上调用的方法(以及事件add_XXX/remove_XXX实际上是方法)。好吧,很酷,所以现在我们所要做的就是调用原始委托(Action<T>
),传入我们创建的短暂代理,然后找出在它上面调用了哪些方法。就这么简单。
我确实花了不少时间才想到这个,不过,这基本上就是这个“通用示例”在引发事件时的运作方式。
通用示例:属性
显然,由于我们正在处理一个用于创建模拟对象的DSL,我们必须提供某种方式来使用我们的DSL为我们的模拟对象设置属性,但是我们应该允许什么样的东西呢?
属性可以接受值和返回值,可以抛出异常,也可以引发事件,并且可以被调用一次、两次、多次或从不。因此,以下是我提出的DSL需要支持的属性:
- 我们需要一种方法来设置属性应该返回什么值。
- 我们需要一种方法让Setter抛出
Exception
。我觉得Getter抛出Exception
会很奇怪(好吧,它可能会,但很少见)。 - 我们需要一种方法让Setter可以引发一个事件。我觉得Getter引发事件会非常奇怪。
所以这就是我们的DSL将支持的。现在,让我们继续看看这是如何实现的。
属性:返回值
我们可以使用SetupProperty
方法(这是Mock类的链式调用方法之一)来设置属性,如下所示:
public IPropertyData<T> SetupProperty(Expression<Func<T, Object>> property)
{
PropertyData<T> propertyData = GetPropertyFromExpression(property);
allPropertiesForProxy.Add(propertyData);
return propertyData;
}
以下是如何直接在模拟对象上设置带返回值的属性:
public IPropertyData<T> SetupProperty(Expression<Func<T, Object>> property, object returnValue)
{
PropertyData<T> propertyData = GetPropertyFromExpression(property);
propertyData.Returns(returnValue);
allPropertiesForProxy.Add(propertyData);
return propertyData;
}
在这两种情况下,都使用了以下辅助方法来创建一个新的PropertyData<T>
构建器。
private PropertyData<T> GetPropertyFromExpression(Expression<Func<T, Object>> property)
{
if (property is LambdaExpression)
{
PropertyData<T> propertyData = new PropertyData<T>();
((IInterceptablePropertyData)propertyData).Proxy = proxy;
propertyData.Mock = this;
((IInterceptablePropertyData)propertyData).Property = ExpressionHelper.GetProperty(property);
return propertyData;
}
throw new InvalidOperationException("Could not create Setup for this property");
}
我们还使用此实用代码从原始Expression<Func<T, Object>>
中获取PropertyInfo
。
public static PropertyInfo GetProperty<T>(Expression<Func<T, Object>> propertyExpression)
{
var lambda = propertyExpression as LambdaExpression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambda.Body as MemberExpression;
}
var propertyInfo = memberExpression.Member as PropertyInfo;
return propertyInfo;
}
返回的类型是PropertyData<T>
,这是一个属性构建器,用于配置模拟对象的其余属性级数据。
通过使用PropertyData<T>
构建器,我们还可以使用Returns
方法设置返回值,如下所示:
internal sealed class PropertyData<T> : IPropertyData<T>, IInterceptablePropertyData, ISupportExceptions
{
private object returnValue;
public IPropertyData<T> Returns(object returnValue)
{
this.returnValue = returnValue;
return this;
}
}
以下是如何在你自己的代码中使用DSL片段:
//Setup return value inline
Mock<ITestClass> mockPropCase1 = new Mock<ITestClass>();
mockPropCase1.SetupProperty(x => x.IntGetSetProperty, 1);
Console.WriteLine(string.Format("IntGetSetProperty={0}", mockPropCase1.Object.IntGetSetProperty));
//Setup return using Returns method
Mock<ITestClass> mockPropCase2 = new Mock<ITestClass>();
mockPropCase2.SetupProperty(x => x.IntGetSetProperty).Returns(3);
Console.WriteLine(string.Format("IntGetSetProperty={0}", mockPropCase2.Object.IntGetSetProperty));
//Setup return value by directly writing to property value
Mock<ITestClass> mockPropCase3 = new Mock<ITestClass>();
mockPropCase3.SetupProperty(x => x.IntGetSetProperty);
mockPropCase3.Object.IntGetSetProperty = 5;
Console.WriteLine(string.Format("IntGetSetProperty={0}", mockPropCase3.Object.IntGetSetProperty));
属性:抛出异常
属性能够抛出Exception
可能只在属性的Setter中有意义,毕竟谁想在每次尝试读取属性时都抛出Exception
呢。那么这如何实现呢?嗯,事实证明这非常简单。我们只是提供一个DSL片段,它接受一个通用的Type
(用于抛出的Exception
,使用Activator.CreateInstance
创建)或接受一个预先填充的Exception
对象。
无论哪种情况,我们都将Exception
存储在PropertyData<T>
构建器中,并在PropertyInterceptor
中使用此Exception
,通过实现ISupportExceptions
接口,该接口也是PropertyData<T>
构建器类实现的。
internal sealed class PropertyData<T> : IPropertyData<T>, IInterceptablePropertyData, ISupportExceptions
{
private Exception exceptionToThrow;
public PropertyData()
{
eventsToRaise = new List<EventWrapper>();
setHasBeenCalled = 0;
getHasBeenCalled = 0;
exceptionToThrow = null;
}
public IPropertyData<T> ThrowsOnSet<TEx>() where TEx : Exception
{
exceptionToThrow = (Exception)Activator.CreateInstance<TEx>();
return this;
}
public IPropertyData<T> ThrowsOnSet(Exception ex)
{
exceptionToThrow = ex;
return this;
}
#region ISupportExceptions members
Exception ISupportExceptions.ExceptionToThrow
{
get { return exceptionToThrow; }
set { exceptionToThrow = value; }
}
bool ISupportExceptions.HasException
{
get { return exceptionToThrow != null; }
}
#endregion
}
然后,在我们设置属性时,我们可以在PropertyInterceptor
中检查是否应该引发异常。正如我多次提到的,我正在使用CastleInterceptors。以下是PropertyInterceptor
的相关部分。
internal class PropertyInterceptor
{
public static void Intercept(IMock parentMock, IInvocation invocation)
{
List<IInterceptablePropertyData> allPropertiesForProxy = parentMock.AllPropertiesForProxy;
string invocationPropertyName = invocation.Method.Name.Substring(4);
invocationPropertyName.Replace("()", "");
List<IInterceptablePropertyData> propertyDataItems =
allPropertiesForProxy.Where(x => x.Property.Name == invocationPropertyName).ToList();
if (!propertyDataItems.Any())
throw new InvalidOperationException(string.Format(
"Property '{0}' was not found and is needed for this Mock",
invocationPropertyName));
if (propertyDataItems.Count() != 1)
throw new InvalidOperationException(string.Format("Property '{0}' was " +
"found more than once for this Mock", invocationPropertyName));
IInterceptablePropertyData propertyData = propertyDataItems.Single();
//Deal with actual method setter call
if (invocation.Method.Name.StartsWith("set_"))
{
//Deal with Events for the method
propertyData.RaiseEvents();
//Deal with Exceptions for the property
ExceptionHelper.ThrowException((ISupportExceptions)propertyData);
....
....
}
....
....
....
}
}
以下是如何在你自己的代码中使用DSL片段:
//Setup which will throw your expected type of Exception/Message on property Setter being called
Mock<ITestClass> mockPropCase4 = new Mock<ITestClass>();
mockPropCase4.SetupProperty(x => x.IntGetSetProperty).ThrowsOnSet(
new InvalidOperationException("this is from the mock property setter"));
try
{
mockPropCase4.Object.IntGetSetProperty = 5;
}
catch (InvalidOperationException ex)
{
Console.WriteLine(string.Format("Exception seen. Message was : '{0}'", ex.Message));
}
//Setup which will throw your expected type of Exception on property Setter being called
Mock<ITestClass> mockPropCase4b = new Mock<ITestClass>();
mockPropCase4b.SetupProperty(x => x.IntGetSetProperty).ThrowsOnSet<InvalidOperationException>();
try
{
mockPropCase4b.Object.IntGetSetProperty = 5;
}
catch (InvalidOperationException ex)
{
Console.WriteLine(string.Format("Exception seen. Message was : '{0}'", ex.Message));
}
属性:引发事件
现在我已经讲完了如何以强类型的方式推断事件名称(请参阅常用技术:查找事件名称),让我们专注于查看属性构建DSL片段如何实际处理事件。
它始于能够以强类型的方式接受事件,这一点我们上面已经讨论过,但为了完整起见,以下是接受标准事件参数签名来引发事件的方法,以及自定义事件参数签名:
public IPropertyData<T> RaiseEventOnSet(Action<T> eventToRaise, EventArgs eventArgs)
{
MemberInfo member = eventRaiserHelper.GetEvent((IEventRaisingAgent)Mock, proxy, eventToRaise);
eventsToRaise.Add(new EventWrapper(member, new object[] { eventArgs }, false));
return this;
}
public IPropertyData<T> RaiseEventOnSet(Action<T> eventToRaise, params object[] args)
{
MemberInfo member = eventRaiserHelper.GetEvent((IEventRaisingAgent)Mock, proxy, eventToRaise);
eventsToRaise.Add(new EventWrapper(member, args, true));
return this;
}
这里实际上只是使用了以下EventHelper
代码:
public class EventRaiserHelper<T>
{
public MemberInfo GetEvent(IEventRaisingAgent eventRaisingAgent, object proxy, Action<T> eventToRaise)
{
PredictiveAnalyzer<T> predictiveAnalyzer =
new PredictiveAnalyzer<T>(proxy, eventToRaise, AnalyzerType.Event);
predictiveAnalyzer.Analyze();
return predictiveAnalyzer.Invocation;
}
....
....
....
....
}
以下是PredictiveAnalyser
的完整代码:
public interface IPredictiveAnalyzer
{
MemberInfo Invocation { get; set; }
}
public class PredictiveAnalyzer<T> : IPredictiveAnalyzer
{
private object existingProxy;
private Action<T> eventToRaise;
private AnalyzerType analyzerType;
public PredictiveAnalyzer(object existingProxy,
Action<T> eventToRaise, AnalyzerType analyzerType)
{
this.existingProxy = existingProxy;
this.eventToRaise = eventToRaise;
this.analyzerType = analyzerType;
}
public void Analyze()
{
ProxyGenerator generator = new ProxyGenerator();
IInterceptor[] interceptors = new IInterceptor[] { new PredictiveInterceptor(
existingProxy, (IPredictiveAnalyzer)this, analyzerType) };
T predictiveProxy = (T)generator.CreateInterfaceProxyWithoutTarget(typeof(T), interceptors);
eventToRaise(predictiveProxy);
}
public MemberInfo Invocation { get; set; }
}
这样我们就得到了一个要存储的事件名称,我们将其存储在List<EventWrapper>
对象中以供将来使用。
但是如何引发这些实际的事件呢?到目前为止,我们所做的只是说,当调用属性XYZ的setter时,我们想以强类型的方式引发事件ABC。但是,当我们属性值确实改变时,我们如何实际调用正确的事件调用列表回调处理程序呢?
要理解那一部分,我们需要回想一下,实际上有一个EventInterceptor
在起作用,它被强制应用于DSL正在创建的实际模拟对象。这是那段代码,可以看到我们存储回调委托在一个IEventRaisingAgent
(这是DSL正在构建的实际模拟对象)上。
internal enum HandlerOperation { Add, Remove }
internal class EventInterceptor
{
private static Object locker = new Object();
public static void Intercept(IEventRaisingAgent parentMock,
IInvocation invocation, HandlerOperation handlerOperation)
{
lock(locker)
{
string rawEventName = invocation.Method.Name;
string eventName = invocation.Method.Name.Substring(invocation.Method.Name.IndexOf("_") + 1);
switch(handlerOperation)
{
case HandlerOperation.Add:
parentMock.AllEventsForProxy.Add(eventName, new EventData(eventName));
parentMock.AllEventsForProxy[eventName].AddHandler((Delegate)invocation.Arguments[0]);
break;
case HandlerOperation.Remove:
parentMock.AllEventsForProxy[eventName].RemoveHandler((Delegate)invocation.Arguments[0]);
break;
}
}
return;
}
}
这样我们就知道了要调用哪些事件回调委托。但是我们如何实际调用它们呢?嗯,对于这个难题,我们需要跳回到PropertyInterceptor
,其中与事件相关最相关的部分如下所示:
internal class PropertyInterceptor
{
public static void Intercept(IMock parentMock, IInvocation invocation)
{
List<IInterceptablePropertyData> allPropertiesForProxy = parentMock.AllPropertiesForProxy;
string invocationPropertyName = invocation.Method.Name.Substring(4);
invocationPropertyName.Replace("()", "");
List<IInterceptablePropertyData> propertyDataItems =
allPropertiesForProxy.Where(x => x.Property.Name == invocationPropertyName).ToList();
if (!propertyDataItems.Any())
throw new InvalidOperationException(string.Format(
"Property '{0}' was not found and is needed for this Mock",
invocationPropertyName));
if (propertyDataItems.Count() != 1)
throw new InvalidOperationException(string.Format(
"Property '{0}' was found more than once for this Mock",
invocationPropertyName));
IInterceptablePropertyData propertyData = propertyDataItems.Single();
//Deal with actual method setter call
if (invocation.Method.Name.StartsWith("set_"))
{
//Deal with Events for the method
propertyData.RaiseEvents();
....
....
....
....
}
....
....
....
}
}
可以看到,这只是为当前属性调用调用DSL存储的PropertyData<T>.RaiseEvents()
方法。所以,为了完成这个巧妙的网络,我们需要做的就是看看PropertyData<T>.RaiseEvents()
,如下所示:
void IInterceptablePropertyData.RaiseEvents()
{
foreach (EventWrapper eventWrapper in eventsToRaise)
{
eventRaiserHelper.RaiseEvent((IEventRaisingAgent)Mock,
eventWrapper.IsCustomEvent, proxy, eventWrapper.Args, eventWrapper.Member);
}
}
这里调用了EventHelper.RaiseEvent()
方法,它看起来像这样:
public class EventRaiserHelper<T>
{
public void RaiseEvent(IEventRaisingAgent eventRaisingAgent,
bool isCustomEvent, object proxy, object[] args, MemberInfo member)
{
List<object> delegateArgs = new List<object>();
if (!isCustomEvent)
{
delegateArgs.Add(proxy);
}
delegateArgs.AddRange(args);
if (eventRaisingAgent.AllEventsForProxy.ContainsKey(member.Name))
{
foreach (Delegate handler in eventRaisingAgent.AllEventsForProxy[member.Name].InvocationList)
{
handler.Method.Invoke(handler.Target, delegateArgs.ToArray());
}
}
}
}
这就是DSL如何支持引发模拟事件。很简单,对吧?
属性:验证
在使用此“通用示例”DSL构建的模拟对象时,并非不可能知道某个属性被调用了多少次。在“通用示例”中,这称为验证。
这是基本思路:
- 每次我们看到属性getter/setter被调用时,我们都会在DSL所代表的模拟对象内部增加一个计数器。
- 在DSL创建的模拟对象使用完毕后(例如在单元测试中),我们可以验证属性。
这是属性构建器PropertyData<T>
的相关代码:
internal sealed class PropertyData<T> : IPropertyData<T>, IInterceptablePropertyData, ISupportExceptions
{
private int setCallLimit;
private int setHasBeenCalled;
private int getCallLimit;
private int getHasBeenCalled;
public PropertyData()
{
....
....
setHasBeenCalled = 0;
getHasBeenCalled = 0;
....
....
}
int IInterceptablePropertyData.SetHasBeenCalled
{
get { return setHasBeenCalled; }
set { setHasBeenCalled = value; }
}
int IInterceptablePropertyData.GetHasBeenCalled
{
get { return getHasBeenCalled; }
set { getHasBeenCalled = value; }
}
}
以下是处理存储属性调用次数的PropertyInterceptor
的相关代码:
internal class PropertyInterceptor
{
public static void Intercept(IMock parentMock, IInvocation invocation)
{
List<IInterceptablePropertyData> allPropertiesForProxy = parentMock.AllPropertiesForProxy;
string invocationPropertyName = invocation.Method.Name.Substring(4);
invocationPropertyName.Replace("()", "");
List<IInterceptablePropertyData> propertyDataItems =
allPropertiesForProxy.Where(x => x.Property.Name == invocationPropertyName).ToList();
if (!propertyDataItems.Any())
throw new InvalidOperationException(string.Format(
"Property '{0}' was not found and is needed for this Mock",
invocationPropertyName));
if (propertyDataItems.Count() != 1)
throw new InvalidOperationException(string.Format(
"Property '{0}' was found more than once for this Mock",
invocationPropertyName));
IInterceptablePropertyData propertyData = propertyDataItems.Single();
//Deal with actual method setter call
if (invocation.Method.Name.StartsWith("set_"))
{
....
....
....
propertyData.SetHasBeenCalled++;
}
//Deal with actual method setter call
if (invocation.Method.Name.StartsWith("get_"))
{
....
....
....
propertyData.GetHasBeenCalled++;
}
....
....
....
}
}
所以你可以看到这个过程真的很简单,我们只是在属性构建器上直接存储我们希望属性getter/setter被调用的次数,而PropertyInterceptor
验证实际调用特定属性的getter/setter调用的次数也存储在属性构建器中。
以下是一些处理如何使用DSL执行验证的用户代码:
//Verify Setter property calls value
Mock<ITestClass> mockPropCase6 = new Mock<ITestClass>();
mockPropCase6.SetupProperty(x => x.IntGetSetProperty);
mockPropCase6.Object.IntGetSetProperty = 10;
mockPropCase6.Object.IntGetSetProperty = 10;
mockPropCase6.Object.IntGetSetProperty = 10;
bool propOk = mockPropCase6.VerifyProperty(x => x.IntGetSetProperty,
SimpleMock.Core.Properties.PropertyType.Setter, WasCalled.ThisManyTimes(2));
string propMsg = propOk ? "Was called correct number of times" :
"Was NOT called correct number of times";
Console.WriteLine(propMsg);
//Verify Setter property calls value
Mock<ITestClass> mockPropCase6b = new Mock<ITestClass>();
mockPropCase6b.SetupProperty(x => x.IntGetSetProperty).Returns(2);
int valueOfProp = mockPropCase6b.Object.IntGetSetProperty;
valueOfProp = mockPropCase6b.Object.IntGetSetProperty;
valueOfProp = mockPropCase6b.Object.IntGetSetProperty;
propOk = mockPropCase6b.VerifyProperty(x => x.IntGetSetProperty,
SimpleMock.Core.Properties.PropertyType.Getter, WasCalled.ThisManyTimes(2));
propMsg = propOk ? "Was called correct number of times" :
"Was NOT called correct number of times";
Console.WriteLine(propMsg);
从上面的代码可以看出,我们可以使用WasCalled
类中的方法来指定验证的调用限制,其中完整的WasCalled
类如下所示:
public class WasCalled
{
public static int Once()
{
return 1;
}
public static int Never()
{
return 0;
}
public static int ThisManyTimes(int thisManyTimes)
{
return thisManyTimes;
}
}
上面显示的用户代码调用了整个模拟对象的VerifyProperty
方法,如下所示:
public bool VerifyProperty(Expression<Func<T, Object>> property, PropertyType propertyType, int callLimit)
{
PropertyData<T> propertyData = GetPropertyFromExpression(property);
IInterceptablePropertyData interceptablePropertyData = allPropertiesForProxy.Where(
x => x.Property.Name == ((IInterceptablePropertyData)propertyData).Property.Name).SingleOrDefault();
if (interceptablePropertyData != null)
{
bool results = false;
switch (propertyType)
{
case PropertyType.Getter:
results = interceptablePropertyData.GetHasBeenCalled <= callLimit;
break;
case PropertyType.Setter:
results = interceptablePropertyData.SetHasBeenCalled <= callLimit;
break;
}
return results;
}
else
throw new MockException("There was a problem finding the property you specified");
}
属性:拦截
正如我多次提到的,我正在使用CastleInterceptors。为了完整起见,以下是PropertyInterceptor
的完整列表:
internal class PropertyInterceptor
{
public static void Intercept(IMock parentMock, IInvocation invocation)
{
List<IInterceptablePropertyData> allPropertiesForProxy = parentMock.AllPropertiesForProxy;
string invocationPropertyName = invocation.Method.Name.Substring(4);
invocationPropertyName.Replace("()", "");
List<IInterceptablePropertyData> propertyDataItems =
allPropertiesForProxy.Where(x => x.Property.Name == invocationPropertyName).ToList();
if (!propertyDataItems.Any())
throw new InvalidOperationException(string.Format(
"Property '{0}' was not found and is needed for this Mock",
invocationPropertyName));
if (propertyDataItems.Count() != 1)
throw new InvalidOperationException(string.Format(
"Property '{0}' was found more than once for this Mock",
invocationPropertyName));
IInterceptablePropertyData propertyData = propertyDataItems.Single();
//Deal with actual method setter call
if (invocation.Method.Name.StartsWith("set_"))
{
//Deal with Events for the method
propertyData.RaiseEvents();
//Deal with Exceptions for the property
ExceptionHelper.ThrowException((ISupportExceptions)propertyData);
propertyData.ReturnValue = invocation.Arguments[0];
propertyData.SetHasBeenCalled++;
}
//Deal with actual method setter call
if (invocation.Method.Name.StartsWith("get_"))
{
propertyData.GetHasBeenCalled++;
}
invocation.ReturnValue = propertyData.ReturnValue;
}
}
这处理了与属性相关的以下问题:
- 引发属性(如果是setter)被调用时注册的任何事件。
- 抛出属性(如果是setter)被调用时注册的异常。
- 维护属性getter/setter被调用次数的计数。
- 返回请求的返回值。
属性:演示
为了说明所有这些特性,让我们考虑以下用户DSL代码:
class Program
{
static void Main(string[] args)
{
#region Property Tests
#region Setup with Return values
//Setup return value inline
Mock<ITestClass> mockPropCase1 = new Mock<ITestClass>();
mockPropCase1.SetupProperty(x => x.IntGetSetProperty, 1);
Console.WriteLine(string.Format("IntGetSetProperty={0}",
mockPropCase1.Object.IntGetSetProperty));
//Setup return using Returns method
Mock<ITestClass> mockPropCase2 = new Mock<ITestClass>();
mockPropCase2.SetupProperty(x => x.IntGetSetProperty).Returns(3);
Console.WriteLine(string.Format("IntGetSetProperty={0}",
mockPropCase2.Object.IntGetSetProperty));
//Setup return value by directly writing to property value
Mock<ITestClass> mockPropCase3 = new Mock<ITestClass>();
mockPropCase3.SetupProperty(x => x.IntGetSetProperty);
mockPropCase3.Object.IntGetSetProperty = 5;
Console.WriteLine(string.Format("IntGetSetProperty={0}",
mockPropCase3.Object.IntGetSetProperty));
#endregion
#region Throw Exception on setter
//Setup which will throw your expected type of Exception/Message on property Setter being called
Mock<ITestClass> mockPropCase4 = new Mock<ITestClass>();
mockPropCase4.SetupProperty(x => x.IntGetSetProperty).ThrowsOnSet(
new InvalidOperationException("this is from the mock property setter"));
try
{
mockPropCase4.Object.IntGetSetProperty = 5;
}
catch (InvalidOperationException ex)
{
Console.WriteLine(string.Format("Exception seen. Message was : '{0}'", ex.Message));
}
//Setup which will throw your expected type of Exception on property Setter being called
Mock<ITestClass> mockPropCase4b = new Mock<ITestClass>();
mockPropCase4b.SetupProperty(x => x.IntGetSetProperty).ThrowsOnSet<InvalidOperationException>();
try
{
mockPropCase4b.Object.IntGetSetProperty = 5;
}
catch (InvalidOperationException ex)
{
Console.WriteLine(string.Format("Exception seen. Message was : '{0}'", ex.Message));
}
#endregion
#region Event Raising on Setter
//Setup which raises event when property set
Mock<ITestClass> mockPropCase5 = new Mock<ITestClass>();
mockPropCase5.SetupProperty(x => x.IntGetSetProperty).RaiseEventOnSet(x => x.Changed += null, new EventArgs());
mockPropCase5.Object.Changed += new EventHandler<EventArgs>(Object_Changed);
mockPropCase5.Object.IntGetSetProperty = 5;
Console.WriteLine(string.Format("IntGetSetProperty={0}", mockPropCase5.Object.IntGetSetProperty));
//Setup which raises event when property set
Mock<ITestClass> mockPropCase5b = new Mock<ITestClass>();
mockPropCase5b.SetupProperty(x => x.IntGetSetProperty).RaiseEventOnSet(x => x.CustomEvent += null, 99, 101);
mockPropCase5b.Object.CustomEvent += new CustomIntEventHandler(Object_CustomEvent);
mockPropCase5b.Object.IntGetSetProperty = 5;
Console.WriteLine(string.Format("IntGetSetProperty={0}", mockPropCase5b.Object.IntGetSetProperty));
#endregion
#region Verification
//Verify Setter property calls value
Mock<ITestClass> mockPropCase6 = new Mock<ITestClass>();
mockPropCase6.SetupProperty(x => x.IntGetSetProperty);
mockPropCase6.Object.IntGetSetProperty = 10;
mockPropCase6.Object.IntGetSetProperty = 10;
mockPropCase6.Object.IntGetSetProperty = 10;
bool propOk = mockPropCase6.VerifyProperty(x => x.IntGetSetProperty,
SimpleMock.Core.Properties.PropertyType.Setter, 2);
string propMsg = propOk ? "Was called correct number of times" :
"Was NOT called correct number of times";
Console.WriteLine(propMsg);
//Verify Setter property calls value
Mock<ITestClass> mockPropCase6b = new Mock<ITestClass>();
mockPropCase6b.SetupProperty(x => x.IntGetSetProperty).Returns(2);
int valueOfProp = mockPropCase6b.Object.IntGetSetProperty;
valueOfProp = mockPropCase6b.Object.IntGetSetProperty;
valueOfProp = mockPropCase6b.Object.IntGetSetProperty;
propOk = mockPropCase6b.VerifyProperty(x => x.IntGetSetProperty,
SimpleMock.Core.Properties.PropertyType.Getter, 2);
propMsg = propOk ? "Was called correct number of times" :
"Was NOT called correct number of times";
Console.WriteLine(propMsg);
#endregion
#endregion
Console.ReadLine();
}
static void Object_CustomEvent(int arg1, int arg2)
{
Console.WriteLine(string.Format("Object_CustomEvent called with {0},{1}", arg1, arg2));
}
static void Object_Changed(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Object_Changed called with {0}",e));
}
static void Object_Changed2(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Object_Changed2 called with {0}", e));
}
}
现在让我们看看它的输出:
通用示例:方法
显然,由于我们正在处理一个用于创建模拟对象的DSL,我们必须提供某种方式来使用我们的DSL为我们的模拟对象设置方法。但是我们应该允许什么样的东西呢?
方法可以接受值,可以只接受有效值,并且可以返回值,并且可以抛出异常,也可以引发事件,并且可以被调用一次、两次、多次或从不。因此,以下是我提出的DSL需要为方法支持的内容:
- 我们需要一种方法来设置方法应该返回什么值。
- 我们需要一种方法来设置将传递给方法的参数。
- 我们需要一种方法在方法被调用时回调用户代码。
- 我们需要一种方法来抛出
Exception
。 - 我们需要一种方法让方法可以引发事件。
所以这就是我们的DSL将支持的。现在,让我们继续看看这是如何实现的。
值得注意的是,我们将要讨论的许多方法功能与我们之前讨论过的属性功能几乎相同,唯一区别是我们将在方法中使用以下两个数据结构,而不是我们之前为属性使用的结构:
MethodData<T>
:用于在语义模型内部创建/存储方法数据的用于方法的构建器。MethodInterceptor
:处理方法级别调用拦截的整体拦截器。
为了完整起见,以下是这两个类的完整列表:
MethodData<T>
internal sealed class MethodData<T> : IMethodData<T>, IInterceptableMethodData, ISupportExceptions
{
private List<IArgumentChecker> argumentCheckers;
private object proxy;
private object returnValue;
private ICallbackInvoker callback;
private MethodInfo method;
private int callLimit;
private int hasBeenCalled;
private List<EventWrapper> eventsToRaise;
private Exception exceptionToThrow;
private EventRaiserHelper<T> eventRaiserHelper = new EventRaiserHelper<T>();
public MethodData()
{
argumentCheckers = new List<IArgumentChecker>();
eventsToRaise = new List<EventWrapper>();
hasBeenCalled = 0;
exceptionToThrow = null;
}
public IMock Mock { get; set; }
public IMethodData<T> Returns(object returnValue)
{
this.returnValue = returnValue;
return this;
}
public IMethodData<T> IsCalled(int callLimit)
{
this.callLimit = callLimit;
return this;
}
public IMethodData<T> WithCallback<T1>(Expression<Action<T1>> callbackDelegate)
{
callback = new ActionCallbackInvokerOne<T1>(callbackDelegate);
return this;
}
public IMethodData<T> WithCallback<T1, T2>(Expression<Action<T1, T2>> callbackDelegate)
{
callback = new ActionCallbackInvokerTwo<T1,T2>(callbackDelegate);
return this;
}
public IMethodData<T> WithPropertyBagCallback(Expression<Action<DynamicWrapper>> callbackDelegate)
{
callback = new ActionCallbackInvokerDynamic(callbackDelegate);
return this;
}
public IMethodData<T> RaiseEvent(Action<T> eventToRaise, EventArgs eventArgs)
{
MemberInfo member = eventRaiserHelper.GetEvent((IEventRaisingAgent)Mock, proxy, eventToRaise);
eventsToRaise.Add(new EventWrapper(member, new object[] { eventArgs}, false));
return this;
}
public IMethodData<T> RaiseEvent(Action<T> eventToRaise, params object[] args)
{
MemberInfo member = eventRaiserHelper.GetEvent((IEventRaisingAgent)Mock, proxy, eventToRaise);
eventsToRaise.Add(new EventWrapper(member, args, true));
return this;
}
public IMethodData<T> Throws<TEx>() where TEx : Exception
{
exceptionToThrow = (Exception)Activator.CreateInstance<TEx>();
return this;
}
public IMethodData<T> Throws(Exception ex)
{
exceptionToThrow = ex;
return this;
}
#region IInterceptableMethodData members
List<IArgumentChecker> IInterceptableMethodData.ArgumentCheckers
{
get { return argumentCheckers; }
set { argumentCheckers = value; }
}
object IInterceptableMethodData.Proxy
{
get { return proxy; }
set { proxy = value; }
}
object IInterceptableMethodData.ReturnValue
{
get { return returnValue; }
set { returnValue = value; }
}
ICallbackInvoker IInterceptableMethodData.Callback
{
get { return callback; }
set { callback = value; }
}
MethodInfo IInterceptableMethodData.Method
{
get { return method; }
set { method = value; }
}
int IInterceptableMethodData.HasBeenCalled
{
get { return hasBeenCalled; }
set { hasBeenCalled = value; }
}
void IInterceptableMethodData.RaiseEvents()
{
foreach (EventWrapper eventWrapper in eventsToRaise)
{
eventRaiserHelper.RaiseEvent((IEventRaisingAgent)Mock,
eventWrapper.IsCustomEvent, proxy, eventWrapper.Args, eventWrapper.Member);
}
}
#endregion
#region ISupportExceptions members
Exception ISupportExceptions.ExceptionToThrow
{
get { return exceptionToThrow; }
set { exceptionToThrow = value; }
}
bool ISupportExceptions.HasException
{
get { return exceptionToThrow != null; }
}
#endregion
}
MethodInterceptor
internal class MethodInterceptor
{
public static void Intercept(IMock parentMock, IInvocation invocation)
{
List<IInterceptableMethodData> allMethodsForProxy = parentMock.AllMethodsForProxy;
List<IInterceptableMethodData> methodDataItems =
allMethodsForProxy.Where(x => x.Method.Name == invocation.Method.Name).ToList();
if (!methodDataItems.Any())
throw new InvalidOperationException(string.Format(
"Method '{0}' was not found and is needed for this Mock",
invocation.Method.Name));
if (methodDataItems.Count() != 1)
throw new InvalidOperationException(string.Format(
"Method '{0}' was found more than once for this Mock",
invocation.Method.Name));
IInterceptableMethodData methodData = methodDataItems.Single();
//Deal with Exceptions for the method
ExceptionHelper.ThrowException((ISupportExceptions)methodData);
//Deal with Events for the method
methodData.RaiseEvents();
//Deal with actual method call
methodData.HasBeenCalled++;
//validate using the argument checker
for (int i = 0; i < invocation.Arguments.Length; i++)
{
IArgumentChecker checker = methodData.ArgumentCheckers[i];
if (!checker.CheckArgument(invocation.Arguments[i]))
{
throw new InvalidOperationException(string.Format(
"Method '{0}' was called with invalid arguments",
invocation.Method.Name));
}
}
//now do the callbacks
if (methodData.Callback is IIsDynamicallbackInvoker)
{
ParameterInfo[] methodParams = invocation.Method.GetParameters().ToArray();
DynamicWrapper wrapper = new DynamicWrapper();
for (int i = 0; i < methodParams.Length; i++)
{
wrapper[methodParams[i].Name] = invocation.Arguments[i];
}
methodData.Callback.InvokeCallback(new object[] { wrapper });
}
else
{
methodData.Callback.InvokeCallback(invocation.Arguments);
}
invocation.ReturnValue = methodData.ReturnValue;
}
}
方法:MethodData<T> 构建器是如何创建的
由于处理方法的概念大部分已经涵盖了我们之前讨论过的属性的概念,因此本节将只详细介绍与我们之前看到的属性不同的部分。
一个直接的区别是,支撑“通用示例”DSL的模拟对象具有用于处理方法的MethodData<T>
构建器对象,而不是我们之前在处理属性时看到的PropertyData<T>
。那么,这些MethodData<T>
构建器对象最初是如何创建的呢?
嗯,它从基本的DSL方法语法开始,如下所示:
Mock<ITestClass> mockCase4b = new Mock<ITestClass>();
mockCase4b.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()));
string case4b = mockCase4b.Object.PrintSomething(1, 3);
它使用了以下模拟代码:
public class Mock<T> : IMock, IEventRaiser<T>, IEventRaisingAgent
{
private T proxy;
private List<IInterceptableMethodData> allMethodsForProxy = new List<IInterceptableMethodData>();
public Mock()
{
proxy = CreateProxy<T>();
}
public IMethodData<T> Setup(Expression<Action<T>> method)
{
MethodData<T> methodData = GetMethodFromExpression(method.Body);
((IInterceptableMethodData)methodData).ArgumentCheckers =
GetArgumentCheckers(methodData, method.ToLambda().ToMethodCall()); ;
allMethodsForProxy.Add(methodData);
return methodData;
}
public IMethodData<T> Setup(Expression<Func<T, Object>> method)
{
MethodData<T> methodData = GetMethodFromExpression(method.Body);
((IInterceptableMethodData)methodData).ArgumentCheckers =
GetArgumentCheckers(methodData, method.ToLambda().ToMethodCall()); ;
allMethodsForProxy.Add(methodData);
return methodData;
}
private MethodData<T> GetMethodFromExpression(Expression expr)
{
if (expr is MethodCallExpression)
{
MethodCallExpression methodCallExpression = expr as MethodCallExpression;
MethodData<T> methodData = new MethodData<T>();
((IInterceptableMethodData)methodData).Proxy = proxy;
methodData.Mock = this;
((IInterceptableMethodData)methodData).Method = methodCallExpression.Method;
return methodData;
}
throw new InvalidOperationException("Could not create Setup for this method");
}
}
其中Setup
方法都接受一个Expression
,该表达式包含要调用的方法,而该方法是通过上面显示的GetMethodFromExpression
辅助方法获得的。
以下每个子部分都利用了这些MethodData<T>
方法构建器对象,这些对象是由使用此代码获得的模拟流畅接口返回的。
方法:验证参数
我们可能希望在DSL中允许的一个很酷的功能是指定可以应用于方法参数值的参数检查器,这样当方法实际被调用时,方法调用将通过DSL最初为特定方法设置的任何参数检查器,然后再进行方法调用。如果为当前调用在DSL中设置的任何参数检查器失败,则会抛出InvalidOperationException
,表明为被调用的方法提供了无效参数。
那么这是如何工作的呢?嗯,和以前一样,我们从查看实际的DSL语法开始,它看起来像这样:
Mock<ITestClass> mockCase4b = new Mock<ITestClass>();
mockCase4b.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()));
string case4b = mockCase4b.Object.PrintSomething(1, 3);
这是一个典型的参数检查器。
那么这是如何工作的呢?嗯,和以前一样,我们从查看实际的DSL语法开始,它看起来像这样:
It.Is<int>((value) => value == 1), It.IsAny<int>()
如果我们查看It
类,可能会更容易理解这里发生了什么。
public class It
{
public static T IsAny<T>()
{
return default(T);
}
public static T Is<T>(Predicate<T> pred)
{
return default(T);
}
}
这是一个非常简单的类,那么这些参数验证器是如何创建的呢?
它们的创建实际上发生在Mock
类中我们之前看到的Setup
方法中,这两个方法最终都会调用这个辅助方法,该方法返回一个List<IArgumentChecker>
对象,该对象表示DSL当前正在构建的方法的参数检查器。
private List<IArgumentChecker> GetArgumentCheckers(MethodData<T> methodData, MethodCallExpression methodCall)
{
Expression[] arguments = methodCall.Arguments.ToArray<Expression>();
List<IArgumentChecker> currentArgumentCheckerSet = new List<IArgumentChecker>();
for (int i = 0; i < arguments.Count(); i++)
{
if (arguments[i] is MethodCallExpression)
{
IArgumentChecker argumentChecker = ExpressionHelper.GetCheckerFromMethodCallExpression(arguments[i] as MethodCallExpression);
if (argumentChecker != null)
{
currentArgumentCheckerSet.Add(argumentChecker);
}
else
{
throw new InvalidOperationException(string.Format(
"You need to supply Constraints for all arguments for Method {0}",
((IInterceptableMethodData)methodData).Method.Name));
}
}
}
return currentArgumentCheckerSet;
}
其中反过来又使用了以下方法ExpressionHelper.GetCheckerFromMethodCallExpression(..)
。
public static IArgumentChecker GetCheckerFromMethodCallExpression(MethodCallExpression methodCallExpression)
{
List<Type> genericParams = new List<Type>();
IArgumentChecker argumentChecker=null;
genericParams = methodCallExpression.Method.GetGenericArguments().ToList();
if (methodCallExpression.Method.DeclaringType == typeof(It))
{
switch (methodCallExpression.Method.Name)
{
case "IsAny":
argumentChecker = new IsAnyArgumentChecker(genericParams.First());
break;
case "Is":
if (methodCallExpression.Arguments[0] is LambdaExpression)
{
LambdaExpression lambda = (LambdaExpression)methodCallExpression.Arguments[0];
if (lambda != null)
{
Type[] lambdaGenParams = new Type[] { lambda.Parameters[0].Type };
var func = lambda.Compile();
var isArgumentCheckerType = typeof(IsArgumentChecker<>).MakeGenericType(lambdaGenParams);
argumentChecker = (IArgumentChecker)Activator.CreateInstance(isArgumentCheckerType, new object[] { func });
}
}
break;
default:
argumentChecker=null;
break;
}
}
return argumentChecker;
}
这段代码的最终结果是我们创建了两种可能的参数检查器之一:我们创建了一个简单的IsAnyArgumentChecker
,如下所示:
public class IsAnyArgumentChecker : IArgumentChecker
{
private Type typeOfIsAnyArgument;
public IsAnyArgumentChecker(Type typeOfIsAnyArgument)
{
this.typeOfIsAnyArgument = typeOfIsAnyArgument;
}
public bool CheckArgument(object argument)
{
return argument.GetType().IsAssignableFrom(typeOfIsAnyArgument);
}
}
或者我们创建一个IArgumentChecker
,它将利用传递给DSL方法构建器的原始Predicate<T>
。所以,现在我们已经创建了一些IArgumentChecker
(s)并将它们与MethodData<T>
方法构建器关联起来,这些是如何被用来实际进行验证的呢?嗯,答案很简单,我们只是拦截任何方法调用,然后将传递给该方法的参数通过与方法调用关联的List<IArgumentChecker>
进行处理,看看它们是否都有效。如果无效,则会抛出InvalidOperationException
。以下是MethodInterceptor
的相关部分。
internal class MethodInterceptor
{
public static void Intercept(IMock parentMock, IInvocation invocation)
{
List<IInterceptableMethodData> allMethodsForProxy = parentMock.AllMethodsForProxy;
List<IInterceptableMethodData> methodDataItems =
allMethodsForProxy.Where(x => x.Method.Name == invocation.Method.Name).ToList();
if (!methodDataItems.Any())
throw new InvalidOperationException(string.Format(
"Method '{0}' was not found and is needed for this Mock",
invocation.Method.Name));
if (methodDataItems.Count() != 1)
throw new InvalidOperationException(string.Format(
"Method '{0}' was found more than once for this Mock",
invocation.Method.Name));
IInterceptableMethodData methodData = methodDataItems.Single();
....
....
....
//validate using the argument checker
for (int i = 0; i < invocation.Arguments.Length; i++)
{
IArgumentChecker checker = methodData.ArgumentCheckers[i];
if (!checker.CheckArgument(invocation.Arguments[i]))
{
throw new InvalidOperationException(string.Format(
"Method '{0}' was called with invalid arguments", invocation.Method.Name));
}
}
....
....
....
}
}
方法:返回值
这与属性返回值的工作方式相同,只是我们处理的是MethodData<T>
构建器数据结构和MethodInterceptor
。
以下是如何配置DSL以从用户代码中的方法返回值的示例:
//use callback with known types
Mock<ITestClass> mockCase1 = new Mock<ITestClass>();
mockCase1.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.Returns("HEY IT WORKS");
string case1 = mockCase1.Object.PrintSomething(1, 3);
方法:回调
DSL还支持在DSL代码中提供回调的能力。这些回调通常在DSL中定义如下:
//use callback with known types
Mock<ITestClass> mockCase1 = new Mock<ITestClass>();
mockCase1.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithCallback<int, int>((x, y) => Console.WriteLine(string.Format("Was called with {0} {1}", x, y)))
.Returns("HEY IT WORKS");
string case1 = mockCase1.Object.PrintSomething(1, 3);
//using any number of args for callback
Mock<ITestClass> mockCase2 = new Mock<ITestClass>();
mockCase2.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithPropertyBagCallback((DynamicWrapper propbag) => Console.WriteLine(
string.Format("Was called with {0} {1}", propbag["data"], propbag["data2"])))
.Returns("HEY IT WORKS").RaiseEvent(x => x.Changed += null, new EventArgs());
string case2 = mockCase2.Object.PrintSomething(1, 3);
这里使用的两个DSL链式调用方法是:
WithCallback
WithPropertyBagCallback
你可能会问为什么有两种类型的回调方法链DSL方法可用,你问得对。原因是,由于我们无法预先知道方法的签名,因此我们不知道需要哪种类型的回调委托。当然,我们可以使用Reflection.Emit
创建一个动态委托,这是一个我研究过的途径,但为了让WithCallBack MethodData<T>
构建器正常工作,也需要预先知道该动态委托。
这似乎是一个先有鸡还是先有蛋的问题。这就是为什么我选择了两个独立的回调:WithCallback
处理1或2个参数,然后是WithPropertyBagCallback
,它创建一个具有任意数量参数的动态属性包类型对象。当我最终查看Moq做了什么时,我可以看到这是一个Moq通过包含最多20个参数的委托才解决的问题。虽然我能理解他们为什么这样做,但我不太喜欢。
总之,让我们继续看看处理回调的两个MethodData<T>
构建器方法,好吗?
WithCallback
这是处理我们知道参数类型和数量的简单情况的MethodData<T>
构建器代码:
public IMethodData<T> WithCallback<T1>(Expression<Action<T1>> callbackDelegate)
{
callback = new ActionCallbackInvokerOne<T1>(callbackDelegate);
return this;
}
public IMethodData<T> WithCallback<T1, T2>(Expression<Action<T1, T2>> callbackDelegate)
{
callback = new ActionCallbackInvokerTwo<T1,T2>(callbackDelegate);
return this;
}
这里我们最终创建了这些类型的回调调用程序,这很简单,因为我们知道要创建/使用的回调参数的类型和数量。
internal sealed class ActionCallbackInvokerOne<T> : ICallbackInvoker
{
private readonly Expression<Action<T>> callbackDelegate;
public ActionCallbackInvokerOne(Expression<Action<T>> callbackDelegate)
{
this.callbackDelegate = callbackDelegate;
}
public void InvokeCallback(object[] args)
{
LambdaExpression l = callbackDelegate as LambdaExpression;
Delegate d = l.Compile();
d.DynamicInvoke(args);
}
}
internal sealed class ActionCallbackInvokerTwo<T, T2> : ICallbackInvoker
{
private readonly Expression<Action<T, T2>> callbackDelegate;
public ActionCallbackInvokerTwo(Expression<Action<T, T2>> callbackDelegate)
{
this.callbackDelegate = callbackDelegate;
}
public void InvokeCallback(object[] args)
{
LambdaExpression l = callbackDelegate as LambdaExpression;
Delegate d = l.Compile();
d.DynamicInvoke(args);
}
}
WithPropertyBagCallback
当参数数量未知时,我们诉诸于使用动态对象包装器/调用程序。这是MethodData<T>
构建器代码:
public IMethodData<T> WithPropertyBagCallback(Expression<Action<DynamicWrapper>> callbackDelegate)
{
callback = new ActionCallbackInvokerDynamic(callbackDelegate);
return this;
}
这会创建一个ActionCallbackInvokerDynamic
,它看起来像这样:
internal sealed class ActionCallbackInvokerDynamic : ICallbackInvoker, IIsDynamicallbackInvoker
{
private readonly Expression<Action<DynamicWrapper>> callbackDelegate;
public ActionCallbackInvokerDynamic(Expression<Action<DynamicWrapper>> callbackDelegate)
{
this.callbackDelegate = callbackDelegate;
}
public void InvokeCallback(object[] args)
{
LambdaExpression l = callbackDelegate as LambdaExpression;
Delegate d = l.Compile();
d.DynamicInvoke(args);
}
}
这里的回调中使用的DynamicWrapper
看起来像这样:
public class DynamicWrapper
{
private readonly IDictionary<String, object> propBag = new Dictionary<string, object>();
public object this[string propName]
{
get
{
return propBag[propName];
}
set
{
propBag[propName]=value;
}
}
}
好的,所以现在我们有了所有这些存储在MethodData<T>
构建器中的回调,现在只是一个执行实际回调的问题。和以前一样,这在MethodInterceptor
中完成,这是MethodInterceptor
代码中最相关的一部分:
internal class MethodInterceptor
{
public static void Intercept(IMock parentMock, IInvocation invocation)
{
List<IInterceptableMethodData> allMethodsForProxy = parentMock.AllMethodsForProxy;
List<IInterceptableMethodData> methodDataItems =
allMethodsForProxy.Where(x => x.Method.Name == invocation.Method.Name).ToList();
if (!methodDataItems.Any())
throw new InvalidOperationException(string.Format(
"Method '{0}' was not found and is needed for this Mock", invocation.Method.Name));
if (methodDataItems.Count() != 1)
throw new InvalidOperationException(string.Format(
"Method '{0}' was found more than once for this Mock", invocation.Method.Name));
IInterceptableMethodData methodData = methodDataItems.Single();
....
....
....
....
//now do the callbacks
if (methodData.Callback is IIsDynamicallbackInvoker)
{
ParameterInfo[] methodParams = invocation.Method.GetParameters().ToArray();
DynamicWrapper wrapper = new DynamicWrapper();
for (int i = 0; i < methodParams.Length; i++)
{
wrapper[methodParams[i].Name] = invocation.Arguments[i];
}
methodData.Callback.InvokeCallback(new object[] { wrapper });
}
else
{
methodData.Callback.InvokeCallback(invocation.Arguments);
}
invocation.ReturnValue = methodData.ReturnValue;
}
}
方法:抛出异常
这与从属性抛出异常的工作方式大致相同,只是我们处理的是MethodData<T>
构建器数据结构和MethodInterceptor
。
以下是如何配置DSL以从用户代码中的方法抛出Exception
的示例:
//Throws your expected type of Exception/Message on property Setter being called
try
{
Mock<ITestClass> mockCase5 = new Mock<ITestClass>();
mockCase5.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.Returns("HEY IT WORKS").Throws(new InvalidOperationException("this is from the mock"));
string case5 = mockCase5.Object.PrintSomething(1, 3);
}
catch (InvalidOperationException ex)
{
Console.WriteLine(string.Format("Exception seen. Message was : '{0}'", ex.Message));
}
//Throws your expected type of Exception on property Setter being called
try
{
Mock<ITestClass> mockCase5b = new Mock<ITestClass>();
mockCase5b.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.Returns("HEY IT WORKS").Throws<InvalidOperationException>();
string case5b = mockCase5b.Object.PrintSomething(1, 3);
}
catch (InvalidOperationException ex)
{
Console.WriteLine(string.Format("Exception seen. Message was : '{0}'", ex.Message));
}
方法:引发事件
这与从属性引发事件的工作方式大致相同,只是我们处理的是MethodData<T>
构建器数据结构和MethodInterceptor
。
以下是如何配置DSL以从用户代码中的方法引发事件的示例:
//raising event
Mock<ITestClass> mockCase4 = new Mock<ITestClass>();
mockCase4.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.Returns("HEY IT WORKS").RaiseEvent(x => x.Changed += null, new EventArgs());
mockCase4.Object.Changed += new EventHandler<EventArgs>(Object_Changed);
string case4 = mockCase4.Object.PrintSomething(1, 3);
//raise event directy on mock
mockCase4.RaiseEvent(x => x.Changed += null, new EventArgs());
//using customg event handlers
Mock<ITestClass> mockCase4b = new Mock<ITestClass>();
mockCase4b.Object.CustomEvent += new CustomIntEventHandler(Object_CustomEvent);
mockCase4b.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.Returns("HEY IT WORKS").RaiseEvent(x => x.CustomEvent += null, 99, 101);
string case4b = mockCase4b.Object.PrintSomething(1, 3);
mockCase4b.RaiseEvent(x => x.CustomEvent += null, 101, 99);
static void Object_CustomEvent(int arg1, int arg2)
{
Console.WriteLine(string.Format("Object_CustomEvent called with {0},{1}", arg1, arg2));
}
static void Object_Changed(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Object_Changed called with {0}",e));
}
static void Object_Changed2(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Object_Changed2 called with {0}", e));
}
方法:验证
这与验证属性的工作方式大致相同,只是我们处理的是MethodData<T>
构建器数据结构和MethodInterceptor
。
以下是如何配置DSL以从用户代码中的方法引发事件的示例:
//verify methods
Mock<ITestClass> mockCase6 = new Mock<ITestClass>();
mockCase6.Setup(x => x.PrintSomething(It.IsAny<int>(), It.IsAny<int>()))
.Returns("HEY IT WORKS");
mockCase6.Object.PrintSomething(1, 3);
mockCase6.Object.PrintSomething(1, 3);
bool ok = mockCase6.Verify(x => x.PrintSomething(1, 1), WasCalled.Once());
string msg = ok ? "Was called correct number of times" : "Was NOT called correct number of times";
Console.WriteLine(msg);
方法:拦截
正如我多次提到的,我正在使用CastleInterceptors。与方法拦截相关的拦截器称为MethodInterceptor
,我之前已经展示了它的完整列表。
方法:演示
为了说明所有这些特性,让我们考虑以下用户DSL代码:
class Program
{
static void Main(string[] args)
{
#region Method Tests
#region Callbacks
//use callback with known types
Mock<ITestClass> mockCase1 = new Mock<ITestClass>();
mockCase1.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithCallback<int, int>((x, y) => Console.WriteLine(
string.Format("Was called with {0} {1}", x, y)))
.Returns("HEY IT WORKS");
string case1 = mockCase1.Object.PrintSomething(1, 3);
//using any number of args for callback
Mock<ITestClass> mockCase2 = new Mock<ITestClass>();
mockCase2.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithPropertyBagCallback((DynamicWrapper propbag) => Console.WriteLine(
string.Format("Was called with {0} {1}", propbag["data"], propbag["data2"])))
.Returns("HEY IT WORKS").RaiseEvent(x => x.Changed += null, new EventArgs());
string case2 = mockCase2.Object.PrintSomething(1, 3);
//specifying callback limit
Mock<ITestClass> mockCase3 = new Mock<ITestClass>();
mockCase3.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithPropertyBagCallback((DynamicWrapper propbag) => Console.WriteLine(
string.Format("Was called with {0} {1}", propbag["data"], propbag["data2"])))
.Returns("HEY IT WORKS");
string case3 = mockCase3.Object.PrintSomething(1, 3);
#endregion
#region Raising events
//raising event
Mock<ITestClass> mockCase4 = new Mock<ITestClass>();
mockCase4.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithPropertyBagCallback((DynamicWrapper propbag) => Console.WriteLine(
string.Format("Was called with {0} {1}", propbag["data"], propbag["data2"])))
.Returns("HEY IT WORKS").RaiseEvent(x => x.Changed += null, new EventArgs());
mockCase4.Object.Changed += new EventHandler<EventArgs>(Object_Changed);
string case4 = mockCase4.Object.PrintSomething(1, 3);
//raise event directy on mock
mockCase4.RaiseEvent(x => x.Changed += null, new EventArgs());
//using customg event handlers
Mock<ITestClass> mockCase4b = new Mock<ITestClass>();
mockCase4b.Object.CustomEvent += new CustomIntEventHandler(Object_CustomEvent);
mockCase4b.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithPropertyBagCallback((DynamicWrapper propbag) => Console.WriteLine(
string.Format("Was called with {0} {1}", propbag["data"], propbag["data2"])))
.Returns("HEY IT WORKS").RaiseEvent(x => x.CustomEvent += null, 99, 101);
string case4b = mockCase4b.Object.PrintSomething(1, 3);
mockCase4b.RaiseEvent(x => x.CustomEvent += null, 101, 99);
#endregion
#region Throwing Exceptions
//Throws your expected type of Exception/Message on property Setter being called
try
{
Mock<ITestClass> mockCase5 = new Mock<ITestClass>();
mockCase5.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithPropertyBagCallback((DynamicWrapper propbag) => Console.WriteLine(
string.Format("Was called with {0} {1}", propbag["data"], propbag["data2"])))
.Returns("HEY IT WORKS").Throws(new InvalidOperationException("this is from the mock"));
string case5 = mockCase5.Object.PrintSomething(1, 3);
}
catch (InvalidOperationException ex)
{
Console.WriteLine(string.Format("Exception seen. Message was : '{0}'", ex.Message));
}
//Throws your expected type of Exception on property Setter being called
try
{
Mock<ITestClass> mockCase5b = new Mock<ITestClass>();
mockCase5b.Setup(x => x.PrintSomething(It.Is<int>((value) => value == 1), It.IsAny<int>()))
.WithPropertyBagCallback((DynamicWrapper propbag) => Console.WriteLine(
string.Format("Was called with {0} {1}", propbag["data"], propbag["data2"])))
.Returns("HEY IT WORKS").Throws<InvalidOperationException>();
string case5b = mockCase5b.Object.PrintSomething(1, 3);
}
catch (InvalidOperationException ex)
{
Console.WriteLine(string.Format("Exception seen. Message was : '{0}'", ex.Message));
}
#endregion
#region Method Verification
//verify methods
//use callback with known types
Mock<ITestClass> mockCase6 = new Mock<ITestClass>();
mockCase6.Setup(x => x.PrintSomething(It.IsAny<int>(), It.Is<int>((value) => value == 3)))
.WithCallback<int, int>((x, y) => Console.WriteLine(
"Calling verify method which should only get called once"))
.Returns("HEY IT WORKS");
mockCase6.Object.PrintSomething(1, 3);
try
{
mockCase6.Object.PrintSomething(1, 5);
}
catch (Exception ex)
{
Console.WriteLine("Method was called with incorrect " +
"arguments [{0},{1}], expected [{2},{3}]", 1,5,1,3);
}
bool ok = mockCase6.Verify(x => x.PrintSomething(1, 1), WasCalled.Once());
string msg = ok ? "Was called correct number of times" : "Was NOT called correct number of times";
Console.WriteLine(msg);
#endregion
#endregion
Console.WriteLine("========== END ==========");
Console.ReadLine();
}
static void Object_CustomEvent(int arg1, int arg2)
{
Console.WriteLine(string.Format("Object_CustomEvent called with {0},{1}", arg1, arg2));
}
static void Object_Changed(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Object_Changed called with {0}",e));
}
static void Object_Changed2(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Object_Changed2 called with {0}", e));
}
}
现在让我们看看它的输出:
通用示例:杂项
我想提到的最后一个事情之一是,也可以直接从模拟对象引发事件。这是通过使用以下方法完成的,这些方法允许DSL指定标准事件或自定义事件签名。
public class Mock<T> : IMock, IEventRaiser<T>, IEventRaisingAgent
{
private Dictionary<string, EventData> allEventsForProxy = new Dictionary<string, EventData>();
Dictionary<string, EventData> IEventRaisingAgent.AllEventsForProxy
{
get { return this.allEventsForProxy; }
}
public void RaiseEvent(Action<T> eventToRaise, EventArgs eventArgs)
{
new EventRaiserHelper<T>().GetAndRaiseEvent(this, proxy,
eventToRaise, new object[] { eventArgs });
}
public void RaiseEvent(Action<T> eventToRaise, params object[] args)
{
new EventRaiserHelper<T>().GetAndRaiseEventCustomArgs(this, proxy, eventToRaise, args);
}
}
与之前一样,使用的技术是使用极其短暂的代理,其调用被拦截以获取/存储实际事件信息。这与之前讨论的相同。与直接在模拟对象上引发事件不同的是,我们可以立即引发事件,我们不必等到属性setter或方法调用发生,来确定DSL是否为这些属性/方法提供了要引发的事件。DSL部分字面意思是“立即引发Event_XYZ”。
获取和直接从模拟对象引发事件的代码如下所示。它实际上归结为几个简单的步骤:
- 运行非常短暂的代理,其中表达式树的原始部分(
x => x.Changed += null
)本质上是针对短暂代理执行和拦截的,以从短暂代理MemberInfo
获取事件名称。 - 查看模拟对象中是否有与刚刚请求的事件名称匹配的事件。
- 如果在步骤2中找到了一些事件,只需调用找到的事件名称的
InvocationList
的委托处理程序。
public class EventRaiserHelper<T>
{
public void GetAndRaiseEvent(IEventRaisingAgent eventRaisingAgent, object proxy,
Action<T> eventToRaise, object[] args)
{
List<object> delegateArgs = new List<object>() { proxy };
delegateArgs.AddRange(args);
PredictiveAnalyzer<T> predictiveAnalyzer =
new PredictiveAnalyzer<T>(proxy, eventToRaise, AnalyzerType.Event);
predictiveAnalyzer.Analyze();
MemberInfo member = predictiveAnalyzer.Invocation;
foreach (Delegate handler in eventRaisingAgent.AllEventsForProxy[member.Name].InvocationList)
{
handler.Method.Invoke(handler.Target, delegateArgs.ToArray());
}
}
public void GetAndRaiseEventCustomArgs(IEventRaisingAgent eventRaisingAgent, object proxy,
Action<T> eventToRaise, object[] args)
{
PredictiveAnalyzer<T> predictiveAnalyzer =
new PredictiveAnalyzer<T>(proxy, eventToRaise, AnalyzerType.Event);
predictiveAnalyzer.Analyze();
MemberInfo member = predictiveAnalyzer.Invocation;
foreach (Delegate handler in eventRaisingAgent.AllEventsForProxy[member.Name].InvocationList)
{
handler.Method.Invoke(handler.Target, args);
}
}
}
以下是如何编写DSL的一部分以直接在模拟对象上引发标准事件签名:
Mock<ITestClass> mockCase4 = new Mock<ITestClass>();
mockCase4.Object.Changed += new EventHandler<EventArgs>(Object_Changed);
//raise event directy on mock
mockCase4.RaiseEvent(x => x.Changed += null, new EventArgs());
...
...
static void Object_Changed(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Object_Changed called with {0}",e));
}
以下是如何编写DSL的一部分以直接在模拟对象上引发自定义事件签名:
Mock<ITestClass> mockCase4b = new Mock<ITestClass>();
mockCase4b.Object.CustomEvent += new CustomIntEventHandler(Object_CustomEvent);
//raise event directy on mock
mockCase4b.RaiseEvent(x => x.CustomEvent += null, 101, 99);
...
...
static void Object_CustomEvent(int arg1, int arg2)
{
Console.WriteLine(string.Format("Object_CustomEvent called with {0},{1}", arg1, arg2));
}
通用示例:演示,请仔细查看
我感觉演示比我的文字更能说明问题,请您玩一下。通用演示是“SimpleMock.TestApp”。请玩一下,我希望您能从我包含在演示代码中的示例中理解它是如何工作的。
就是这样
好了,这就是我这次想说的全部内容,希望您喜欢这篇文章,我知道我写这篇文章很享受,因为它并不显而易见如何做某些事情,并在文章/代码的某些地方需要大量的思考。如果您也喜欢,能否请投个赞成票或告诉我您的想法,甚至两者都做?非常感谢。
顺便说一句,我的下一篇文章将是一篇完整的富客户端/服务器端代码一直到数据库的N层文章,它将用于演示人们一直要求我谈论的各种方面/技术,我一直推迟它是因为工作量巨大……时间到了,所以这就是我接下来要做的。
哦,顺便说一句,它将是一个僵尸探测器应用程序,绘制全球僵尸活动图,它将涉及某种地图组件,应该很酷。