65.9K
CodeProject 正在变化。 阅读更多。
Home

LinFu/Cecil 和面向切面编程概念的应用——一个库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (8投票s)

2008年9月5日

CPOL

17分钟阅读

viewsIcon

37013

downloadIcon

176

一个使用面向切面编程概念、并使用 LinFu 和 Cecil.Mono 项目/框架实现的有用功能的库。

引言

本文提出的库利用 LinFu 项目(本身使用 Cecil.Mono)提供的面向切面编程相关功能,来处理多种横切关注点。这些关注点包括:记录方法调用、衡量方法执行时间、重试方法执行、限制或阻止属性值的更改、将属性更改回滚到先前状态、确保方法的单线程执行、断言方法执行前后全局条件、允许方法和属性可观察、以及(最重要但并非最后)记录方法参数和回放方法返回值。

Example

背景:为什么?

我目前在一个小型 IT 团队工作,该团队信奉在编写代码的同时编写单元测试。当我刚加入团队时,我一丝不苟地为我负责的每个模块编写单元测试。很快,我开始注意到单元测试很少被运行,而且有几次当我打开包含所有单元测试的解决方案时,它会因为错误而无法编译。我认为这是开发者在修改依赖代码时没有更新他们的单元测试的问题,于是我着手修复编译错误。当我试图运行单元测试时,几乎所有的测试都失败了……到底怎么了?

大多数单元测试依赖于从数据库检索测试数据。当测试数据被删除或修改时(例如,当生产数据库被复制到我们的测试环境时),单元测试就会失败。因此,由于没有人真正能够“使用”单元测试超过几次,开发者几乎没有动力在添加、更改或删除功能时更新他们的测试代码。

可能的解决方案?

我提出了几种解决方案,这些方案都在网上得到了很好的记录。

  1. 一种解决方案是每次测试前恢复数据库备份。然而,单元测试本来就已经花费很长时间才能运行——即使在剥离了我们的数据库(通常为 3GB)以创建一个较小的测试数据库(200-300MB)之后,每次数据库恢复操作仍然需要一段时间才能完成。
  2. 我研究了编写代码以在数据库不存在时插入适当数据需要什么。然而,大多数单元测试依赖于许多不同表中的大量数据,并且此外还依赖于特定标志的设置或不设置。为每个单元测试编写生成适当测试数据的代码简直是不切实际的。
  3. 作为另一种解决方案,我探索了抽象数据检索方式的想法,以便可以透明地交换数据源。然而,实现这一点(或类似解决方案)将意味着重写我们的数据访问网关层。我当然不愿意重新编写这样一个关键的(而且很大的)组件,它在我们现有的生产环境中已经运行得很好。然而,修改网关从数据源检索数据的概念很有趣……
  4. 我最终偶然发现了 RhinoMock、NMock 和 EasyMock——这些框架旨在为单元测试提供可测试的对象。然而,使用这些框架将涉及修改单元测试代码——这是很难说服开发人员认为值得的。此外,如果开发人员不小心调用了网关方法,我们仍然会遇到从数据库中无法检索到预期数据的情况,导致本应正常工作的代码未能通过单元测试。

理想驱动设计

我最终决定,为了推进任何解决方案,我需要写下我真正想做的事情。我列出了以下需求清单:

  1. 我不想触碰/修改现有的网关代码,也不想我的解决方案以任何方式、形状或形式改变生产代码。
  2. 我希望能够记录从网关方法返回的值。这将包括以下内容:
    • 在单元测试的初始执行期间(数据库中存在正确测试数据时),拦截对任何网关方法的调用。
    • 存储用于调用网关方法的参数,然后存储由网关方法返回的任何值。
    • 将执行单元测试期间使用的所有记录的参数和返回值保存到磁盘。
  3. 我希望能够在稍后重放预先录制的返回值,绕过网关方法的执行。为此,我需要:
    • 在执行单元测试期间拦截对任何网关方法的调用。
    • 当在后续单元测试运行中调用网关方法时,绕过该方法调用。相反,网关方法将返回预先录制的数据——这些数据在编写单元测试时存在于数据库中。
  4. 为了解决单元测试可能获取到陈旧(不切实际/过时)数据的问题,我还希望能够重新录制从数据库返回的数据,然后将新数据回放到单元测试中。

记录参数和返回值

当调用网关方法时,应记录使用的参数值及其返回值以供以后使用,如下图所示。

Example

回放预录制返回值

当在回放期间调用网关方法时,应通过匹配参数值来检索返回值,并完全跳过原始网关方法,如下图所示。

Example

下一步是确定我如何能够动态修改程序流程,以模拟上述图表中显示的两种执行路径之一……

面向切面编程 (AOP)

在 CodeProject、在线书籍和编程网站上浏览时,我偶然发现了面向切面编程 (AOP) 的概念。切面、通知、连接点、切点等,等等,等等……术语都很好,但重要的是,通过使用一种称为“织造”的面向切面编程概念,我可以在测试构建中执行代码——在方法调用之前、之后,或者完全替换方法调用——而对我们的原始源代码的修改最小。此外,通过*不*织造生产构建,我可以确保在我们的生产环境中,现有代码将以未经修改和不受干扰的状态执行。

下一步——我一如既往地对自己说,我会稍后……阅读一些关于这个主题的学术论文……然后开始编写一个使用 AOP 解决我们问题的库。我选择构建在LinFu AOP 库(它使用 Cecil.Mono)之上,我必须感谢这两个项目——如果没有已经完成的大量繁重工作,要完成我想要做的事情会困难得多。

后织造/后构建织造

LinFu 使用一个称为织造的过程,允许程序员定义的代码在方法调用之前、之后和/或代替执行。为此,IL 指令被注入到已编译的程序集中,包装每个原始方法。包装原始方法调用的指令允许在方法执行之前、方法执行之后和/或代替原始方法执行,进行条件分支到程序员定义的代码。

Example

使用示例

在进一步讨论之前,我想展示一些代码片段,它们演示了本文介绍的库的各种功能。

第一个示例显示了一个名为 MakeDeposit 的方法(想象一下:银行交易)。通过用SingleThreadExecution 属性装饰该方法,可以确保该方法不会在多个线程上并发执行。通过用ObservableMethod 属性装饰该方法,可以注册观察者并在调用 MakeDeposit 方法时通知它们。

[SingleThreadExecution("AccountActivity")]
[ObservableMethod]
public double MakeDeposit(double amount)
{
   AccountBalance += amount;
   return AccountBalance;
}

下面展示了 WithdrawMoney 方法。AssertBeforeAfter 属性表示将在执行 WithdrawMoney 之前调用 PreCheckWithdrawl 方法。如果该方法返回false或抛出异常,则可以引发异常或跳过方法执行。PostCheckWithdrawl 属性表示在执行后应调用 PostCheckWithdrawl 方法,以确保在提款过程中没有发生任何不当行为。SingleThreadExecution 属性确保

  1. WithdrawMoney 不会在多个线程上并发执行,并且
  2. WithdrawMoney 不会与任何其他标记了 [SingleThreadExecution("AccountActivity")] 属性的方法(即上面展示的 MakeDeposit 方法)并发执行。
[SingleThreadExecution("AccountActivity")]
[AssertBeforeAfter("PreCheckWithdrawl", 
      "PostCheckWidthdrawl", 
      "Invalid Account Balance", 
      "Withdrawl of ${0} would create a negative balance")]
public double WithdrawMoney(double amount)
{
   AccountBalance -= amount;
   return AccountBalance;
}

在存入 100 美元后,尝试两次提款 60 美元将导致输出类似于下图(下图示例来自演示应用程序)。

Example

在下面的示例中,SSN 属性存储客户的社会安全号码(可能不是个好主意……)。因为该属性标记了RestrictPropertyChange 属性,所以在更改属性值之前将调用 ValidateSSN 方法。如果 ValidateSSN 方法返回false,则不允许更改属性——set {...} 中的代码将完全被绕过。此外,由于 Customer 类实现了 ISupportsPropertyChangeRestrictedNotify,如果属性更改被限制,将引发 PropertyChangeRestricted 事件。(如果类没有实现 ISupportsPropertyChangeRestrictedNotify,则在不通知的情况下阻止更改。)接下来,PropertyChangedAction 属性指定了每当属性值成功更改时应采取的某些操作。在下面的示例中,存储在 Account 属性中的 BankAccount 类实例的 AccountOwnerSSN 属性将自动设置为反映新的 SSN 值。

[AOPEnabled]
public class Customer : ISupportsPropertyChangeRestrictedNotify, ICanIdentifyAsMockObject
{
   public event EventHandler
       <ObjectEventArgs<
           IInvocationContext, PropertyInfo, object, object, Exception>> 
              PropertyChangeRestricted;

   [LogPropertyChanged, RecordPropertyChange]
   public string FirstName { get; set; }

   [LogPropertyChanged, RecordPropertyChange]
   public string LastName { get; set; }

   [RestrictPropertyChange("ValidateSSN"), 
                  LogPropertyChanged, RecordPropertyChange]
   [PropertyChangedAction("Account", "AccountOwnerSSN")]
   public string SSN { get; set; }
      
   private BankAccount _account;
   public BankAccount Account { get { return Populate.OnDemand(ref _account); } }
   
   public static Exception ValidateSSN(
      string propertyName, object oldVal, object newVal, IInvocationContext context)
   {
      try
      {
         if (newVal != null)
         {
            string newString = newVal.ToString();
            long conversionCheck;

            if (!long.TryParse(newString, out conversionCheck) || 
                 newString.Length != 9)
            {
               return new Exception("SSN must be 9 digits");
            }
         }
      }
      catch (Exception ex)
      {
         return ex;
      }

      return null;
   }
}

在上面的示例中,Customer 类有几个属性标记了RecordPropertyChanged 属性。这使我们能够使用库内置的 ChangeTracker 来执行以下操作:

// Set up a customer
Customer customer = new Customer() 
{ 
   FirstName = "Fred", 
   LastName = "Flintstone", 
   SSN = "111992222" 
};

customer.ChangeTracker.ResetPropertyChanges();

// Make a few changes to property values
customer.FirstName = "Freddy";
customer.SSN = "111992223";

// Decided the changes are no good. Revert back to the baseline
customer.ChangeTracker.RevertChangesToLastReset(customer);

// Reverted back to old values - customer.FirstName = 
//        "Fred" and SSN = "111992222"

源代码和解决方案包括几个演示应用程序,它们扩展了上面介绍的代码片段,并演示了库的各种功能。

实现 AOP - 标准示例

为了熟悉 AOP 基础知识,我开始实现我想到的两个最无聊(但仍然有用)的功能——计数方法调用次数和日志记录。具体来说,在日志记录方面,我的目标是在特定方法调用之前和之后都写下一条日志条目。我想要对现有代码进行的唯一更改是向我想要记录日志的方法添加一个属性。示例:

[Logable(LogSeverity.Information, "Before MakeDeposit", "After MakeDeposit")]
public double MakeDeposit(double amount)
{
   ...
}

使用 LinFu 框架的 AOP

使用 LinFu 框架,实现 IAroundInvoke 的类用于挂钩到织造过的程序集。在运行时,IAroundInvokeBeforeInvokeAfterInvoke 方法在原始方法执行之前和之后被调用。

使用实现 IInvocationContext 的类的一个实例,将上下文信息传递给 BeforeInvokeAfterInvoke 方法。IInvocationContext 参数包含诸如正在调用的方法名、传递给方法的参数值以及关于调用上下文的其他信息。要织造一个程序集并启用这种 AOP/方法相关的黑魔法,使用的是 LinFu/Cecil 的 Postweaver;例如,在提供的解决方案中,项目 AOPClasses 构建之后,运行后置构建事件“postweaver.exe $(TargetPath)”。

也许,看到这一切如何工作的最简单方法是展示一个注释良好的示例:下面介绍的 LogWrapper 类是使用 LinFu 的 IAroundInvoke 接口的实现来在方法执行之前和之后写入日志条目的类。

/// <summary>;
/// Hooks into woven assemblies - BeforeInvoke is called before 
/// method invocation (duh), and AfterInvoke is called after.
/// </summary>
public class LogWrapper : AroundMethodBase, IAroundInvoke
{
   /// <summary>
   /// BeforeInvoke is executed before the decorated method is executed
   /// </summary>      
   public override void BeforeInvoke(IInvocationContext context)
   {
      Execute(context, null, CutpointType.Before);
   }

   /// <summary>
   /// AfterInvoke is executed after the decorated method has been executed
   /// </summary>
   public override void AfterInvoke(
      IInvocationContext context, object returnValue)
   {
      Execute(context, returnValue, CutpointType.After);
   }

   private void Execute(
      IInvocationContext context, object returnValue, CutpointType when)
   {
      // The "this" object being used for the method call
      object thisObject = context.Target;

      // Get the "Logable" attributes attach to the method being called
      LogableAttribute[] attrs = 
                         context.TargetMethod.ReadAttribute<LogableAttribute>();

      attrs.ForEachAttribute(delegate(LogableAttribute attr)
      {
         // If before method execution, log the specified "before" message
         // The log message can use {0}...{n} to write out the method's 
         // parameter values...
         if (when == CutpointType.Before && 
             !string.IsNullOrEmpty(attr.MessageType))
         {
            GlobalLogger.Log(
               attr.MessageType,
               String.Format(attr.LogMessage, context.Arguments),
               attr.LogLevel);
         }
         // If after method execution, log the specified "after" message.
         // The log message can use {0} to write out the method's return value
         else if (when == CutpointType.After && 
                  !string.IsNullOrEmpty(attr.MessageTypeAfter))
         {
            GlobalLogger.Log(
               attr.MessageTypeAfter,
               String.Format(attr.LogMessageAfter, returnValue),
               attr.LogLevelAfter);
         }
      });
   }
}

更具挑战性的东西……

这一切都相当容易(而且非常酷),所以我决定尝试实现一个 RestrictPropertyChange 属性,当它附加到属性上时,如果值不满足用户定义的标准,就可以跳过执行用于更改属性值的“set”代码。此外,我还希望以抛出异常或引发事件的形式收到被限制更改的通知。

Example

为此,我需要一种方法来:

  1. 指定(在运行时)跳过原始方法的执行。
  2. 跳过原始方法的执行,而不影响类定义的其他方法的并发执行。

为此,我不得不修改执行后置构建织造的 LinFu 和 Cecil 代码。

  • 我在 IModifiableType 的定义(和后置构建创建)中添加了三个属性:两个对象属性用于存储杂项/附加数据,以及一个布尔属性 IsInterceptionDisabled,用于表示应跳过原始方法调用。
  • 有必要修改 LinFu/Cecil 驱动的织造器,以生成适当的 IL 来检查 IsInterceptionDisabled 的值,并可能跳过原始方法的执行。由以下代码生成的 IL 检查 IsInterceptionDisabled 标志(下面的 _isDisabled),如果为true则分支,跳过原始方法调用的执行。
// Checks the IsInterceptionDisabled flag - used to bypass
// original method execution

instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));

// if IsInterceptionDisabled == false then continue onward
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _isDisabled));
instructions.Enqueue(IL.Create(OpCodes.Brfalse, skipToEnd));

// otherwise go to the post-method call (skip original method execution)
instructions.Enqueue(IL.Create(OpCodes.Br, JumpForDone));

instructions.Enqueue(skipToEnd);

进行这些更改后,我们可以在 BeforeInvoke 中通过在原始方法调用之前将 IsInterceptionDisable 属性设置为true来标记应跳过原始方法的执行。

IModifiableType mod = (context.Target as IModifiableType);
mod.IsInterceptionDisabled = true;

下面是实现了 IAroundInvoke 接口并根据值执行或跳过 set_Property 的类。

public class RestrictPropertyChangeWrapper : AroundMethodBase, IAroundInvoke
{
   public override void BeforeInvoke(IInvocationContext context)
   {
      Execute(context, CutpointType.Before);
   }

   public override void AfterInvoke(IInvocationContext context, object returnValue)
   {
      // If we skipped executing property set {...} this time, restore 
      // any previous IsInterceptionDisabled value that may have been set
      if (context.ExtraInfo != null)
         RestoreIsInterceptionDisabledFlag(context);
   }

   private void Execute(IInvocationContext context, CutpointType when)
   {
      object thisObject = context.Target;
      Type t = thisObject.GetType();

      // Method name comes in as set_PropertyName
      // Grab the property name by taking the substring starting at position 4
      string propInfoName = context.TargetMethod.Name.Substring(4);
      
      // Get property info and other details about the property being set
      PropInfo propInfo = t.GetPropInfo(propInfoName);

      // Get RestrictPropertyChangeAttribute attributes attached to the property
      RestrictPropertyChangeAttribute[] attrs = 
         propInfo.GetAttributes<RestrictPropertyChangeAttribute>();

      if (attrs != null && attrs.Length > 0)
      {
         // Read the old property value and record the new one
         object oldValue = propInfo.GetValue(thisObject);
         object newValue = context.Arguments[0];

         for (int n = 0; n < attrs.Length; n++)
         {
            RestrictPropertyChangeAttribute attr = attrs[n];

            // See if the property change is restricted
            Exception ex = attr.IsRestricted(
               thisObject, t, propInfo.Name, oldValue, newValue, context);

            if (ex != null)
            {
               // Send notification regarding the restriction, if possible
               ISupportsPropertyChangeRestrictedNotify notify = 
                  thisObject as ISupportsPropertyChangeRestrictedNotify;
               if (notify != null)
                  notify.NotifyPropertyChangeRestricted(
                     context, propInfo.PropertyInfo, oldValue, newValue, ex);

               // Mark that the original method should NOT be executed
               SetInterceptionDisabledFlag(context, true);

               if (attr.ThrowOnException)
                  throw ex;
            }
         }
      }
   }

   private void RestoreIsInterceptionDisabledFlag(IInvocationContext context)
   {
      IModifiableType mod = context.Target as IModifiableType;

      // Set the flag back to its original (pre-method-call) value
      mod.IsInterceptionDisabled = Convert.ToBoolean(context.ExtraInfo);

      // Blank out ExtraInfo to mark that we're done using it
      context.ExtraInfo = null;
   }

   private void SetInterceptionDisabledFlag(
      IInvocationContext context, bool value)
   {
      IModifiableType mod = (context.Target as IModifiableType);

      // Store the old value of the IsInterceptionDisabled flag
      context.ExtraInfo = mod.IsInterceptionDisabled;

      // Mark that we do not want to execute the original method's code
      mod.IsInterceptionDisabled = true;
   }
}

在看到 LinFu 和 Cecil 的强大功能之后,我有点疯狂,并实现了以下我认为可能很有用的属性和功能:

  • CountCalls - 统计装饰方法被调用的次数。
  • Logable - 在每次调用装饰方法之前和/或之后添加日志条目。
  • Retry - 如果在执行装饰方法期间发生异常,则重试执行一定次数。
  • TimeMethod - 计时装饰方法执行所需的时间。
  • SingleThreadExecution - 确保方法在任何给定时间不会被多个线程执行。
  • IfCase - 仅在满足某些条件时在方法执行之前执行代码。
  • AssertBeforeAfter - 在方法执行之前和之后检查特定条件是否为真或为假。
  • ObservableMethod - 可以动态注册观察者。当调用装饰方法时,所有注册的观察者都会收到通知。
  • RecordParameters - 调用装饰方法时,会记录方法输入参数和方法返回值。
  • ObservableProperty - 可以动态注册观察者。当设置装饰属性时,所有注册的观察者都会收到通知。
  • LogPropertyChanged - 在装饰属性值更改时,添加一条包含旧值和新属性值的日志条目。
  • PropertyChangedAction - 在装饰属性值更改时执行开发人员定义的事件。
  • RecordPropertyChange - 在装饰属性值更改时记录更改前后的值。
  • RestrictPropertyChange - 在装饰属性值更改之前检查更改前/后的值,如果未满足定义条件,则跳过/绕过更改属性值。

性能开销?

是的,执行织造过的程序集会产生性能开销;所有织造的方法调用(包括属性设置和获取)在原始方法执行之前和之后都会做额外的工作。为了将这种开销限制在绝对需要这些 AOP 相关功能的类上,我定义了 AOPEnabled 属性,并修改了 Postweaver,使其仅织造标记有此属性的类。

[AOPEnabled]
public class BankAccount : ChangeTrackingBaseClass<BankAccount>
{
   ...
}

解决最初的问题:单元测试

我对 AOP 的兴趣始于我在工作中遇到的单元测试/数据不一致问题(在背景部分 - 顶部讨论)。基本上,单元测试很快就变得毫无用处,因为单元测试所依赖的数据经常从数据库中消失或被修改。我想要一种方法来透明地记录方法返回值(在录制模式下)并在(在回放模式下)回放返回值。使用 LinFu/Cecil 和 AOP 概念给了我所有必要的工具——我现在可以:

  1. 在方法执行前拦截方法,并记录传入的参数。
  2. 在将执行返回给调用者之前观察方法的返回值,并且
  3. 选择是否执行原始方法。

以下是它的工作方式:

录制模式

// Turn on return value recording
AOP.EnableAOP(MockMode.Recording);

// Turn off recording for a specific class type only
typeof(MyClass).EnableMockRecording(false);

// Turn back on recording for MyClass class type
typeof(MyClass).EnableMockRecording(true);

在“录制”模式下(针对标记为“Mock-able”的方法):

  • 当调用方法时,将记录传入方法的参数以及方法的返回值。
  • 返回值存储在一个查找表中,可以使用方法名和原始参数值进行检索。
  • 录制完成后,方法名、参数和返回值将序列化到文件中以供以后检索。

回放模式

// Turn on return value playback
AOP.EnableAOP(MockMode.Playback);

// Load previously-recorded return values
MockObjectRepository.AddMockObjectsFromFile("RecordedRetVals.dat");

在“回放”模式下(针对标记为“Mock-able”的方法):

  • 可以从文件中加载和反序列化先前录制的方法名、参数和返回值,并将其添加到查找表中以供快速检索。
  • 当调用一个可模拟的方法时,将使用查找表根据方法名和用于原始调用的相同参数来查找先前录制的回放值。
  • 如果存在匹配的返回值(相同的类类型、相同的方法名、相同的参数),则跳过原始方法的执行,并将先前存储的返回值用作方法调用的返回值。

为了用模拟返回值替换真实返回值,绕过原始方法调用,我不得不在 LinFu/Cecil Postweaver 中添加以下代码:

// Allows mimicking recorded return values without actually
// executing the original method...
instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));

// if ExtraInfoAdditional property is null, continue onward
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _extraInfo));
instructions.Enqueue(IL.Create(OpCodes.Brfalse, skipToEnd));

// otherwise, use the value stored in ExtraInfoAdditional as the 
// return value for the method
instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));

// get the return value stored in the ExtraInfoAdditional property
// note: The property's return value will be used as the 
// original method's return value
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _extraInfo));

// go to the post-method call (skip original method execution)
instructions.Enqueue(IL.Create(OpCodes.Br, JumpForDone));

instructions.Enqueue(skipToEnd);

MockObjectRepository:存储参数和返回值

考虑以下代码:

public List<Customer> FindCustomerUsingPersonalInfo(string customerSSN)
{
   ...
}

public List<Customer> FindCustomerUsingPersonalInfo(
                     string customerSSN, DateTime birthday, string zipCode)
{
   List<Customer> partialMatches = new List<Customer>(
      FindCustomerUsingPersonalInfo(customerSSN));
                              
   partialMatches.RemoveAll(delegate (Customer c) { 
      return (c.Birthday != birthday || c.ZipCode != zipCode); });
                      
   return partialMatches;
}

List<Customer> c1 = FindCustomerUsingPersonalInfo("536187315");

List<Customer> c2 = FindCustomerUsingPersonalInfo(
                     "111223333", new DateTime(1975, 7, 1), "80111");

List<Customer> c3 = FindCustomerUsingPersonalInfo(
                     "555252193", new DateTime(1976, 7, 19), "98225");

List<Customer> c4 = FindCustomerUsingPersonalInfo(
                     "359252491", new DateTime(1972, 4, 19), "80301");

每次调用 FindCustomerUsingPersonalInfo 时,都会传入不同的参数值,并且每次调用(很可能)都会返回不同的客户列表,具体取决于传入的参数值。为了处理由参数值引起的值差异,我使用 StoredParametersAndReturnValue 类的实例来存储标识传递到方法调用中的参数值的哈希码以及给定参数时从方法返回的值。该类还定义了两个方法 PackageMockForStorageUnpackageMockFromStorage,可用于将返回值和参数哈希值序列化和反序列化到/从压缩字节数组。

为了定位特定类类型和方法的返回值,我选择使用以下数据结构来存储参数和返回值:

Example

最基本的是上面讨论的 StoredParametersAndReturnValue 类。在单个存储参数和返回值之上,使用 MockObjectsForMethod 类的实例来存储与单个方法关联的参数哈希和返回值。在下一个级别上,MockObjectsForClass 用于存储单个类类型的 MockObjectsForMethod 集合。最后,使用 MockObjectRepository 的单个实例来存储 MockObjectsForClass 对象的集合。按类类型和方法名存储返回值使我们能够快速钻取与特定类和方法关联的所有录制返回值。

实现方式:录制和检索返回值

MockObjectRepository 类的方法 RecordMockObjectReturnValue 用于在传入特定参数值时,记录与特定类类型和方法名关联的返回值。

public static StoredParametersAndReturnValue RecordMockObjectReturnValue(
          this Type t, string methodName, object[] parameters, object returnValue)
{
   // Retrieve the structure that holds all recorded objects for class type t
   MockObjectsForClass mockObjects = RetrieveMockObjectsForClass(t);

   // Calculate a list of hash values that can be used to 
   // uniquely identify parameter values
   List<int> paramHash = GetParametersHash(parameters);
   int[] paramsHashArray = paramHash.ToArray();

   // Retrieve the structure that holds all recorded objects 
   // for a specific method name
   MockObjectsForMethod methodMocks = 
       mockObjects.GetMockObjectsForMethod(methodName);

   // Find any existing stored parameters/return value associated with 
   // this combo of type, method name, and parameters.
   StoredParametersAndReturnValue found = 
      methodMocks.LookupByParameters.FindItem(paramsHashArray);
   
   if (found == null)
      found = new StoredParametersAndReturnValue();
   else
      methodMocks.LookupByParameters.RemoveTerminatingItem(paramsHashArray);

   // Set up values in StoredParametersAndReturnValue
   found.ListOfParameterHash = paramHash;
   found.Parameters = new List<object>(parameters);
   found.ReturnValue = returnValue;

   // Add this instance of StoredParametersAndReturnValue to 
   // the lookup data structure
   methodMocks.LookupByParameters.AddTerminatingItem(found, paramsHashArray);

   return found;
}

MockObjectRepository 类的方法 StoredParametersAndReturnValue 用于在给定特定参数值时,根据特定的类类型和方法名查找先前录制的返回值。

public static StoredParametersAndReturnValue GetMockObject(
                 this Type t, 
                 string methodName, 
                 params object[] parameters)
{
   // Retrieve the structure that holds all 
   // recorded objects for class type t
   MockObjectsForClass mockObjects = RetrieveMockObjectsForClass(t);

   // Calculate a list of hash values that can be used to 
   // uniquely identify parameter values
   List<int> paramHash = GetParametersHash(parameters);

   // Retrieve the structure that holds all recorded objects 
   // for a specific method name
   MockObjectsForMethod methodMocks = 
      mockObjects.GetMockObjectsForMethod(methodName);

   // Find a pre-recorded return value for this class type, 
   // method name, and parameter values
   StoredParametersAndReturnValue found = 
      methodMocks.LookupByParameters.FindItem(paramHash.ToArray());

   // The return value may need to be unpacked from 
   // compressed binary storage
   if (found != null && found.IsPacked)
      found.UnpackageMockFromStorage(true, true);

   return found;
}

将所有这些功能整合在一起,RecordParametersWrapper 类实现了 IAroundInvoke,可以录制和/或回放返回值,并且能够跳过原始方法调用,同时透明地为调用者提供预录制的返回值。

// RecordParametersWrapper is able to (1) record parameter and return values and/or 
// (2) play back pre-recorded return values in lieu of original method execution
public class RecordParametersWrapper : AroundMethodBase, IAroundInvoke
{
   // Dummy RecordParametersAttribute associated with methods in 
   // ClassMethodsMockable-attribute marked classes
   private static readonly RecordParametersAttribute RecordParamForMocking = 
                               new RecordParametersAttribute(true, true);

   /// <summary>
   /// Substitutes a pre-recorded return value (if one is found) 
   /// in lieu of original method execution
   /// </summary>
   private void UseMockReturnValue(IInvocationContext context, Type t)
   {
      StoredParametersAndReturnValue mock = 
         t.GetMockObject(context.TargetMethod.Name, context.Arguments);

      if (mock != null)
      {
         // Found a pre-recorded return value
         IModifiableType mod = (context.Target as IModifiableType);
         context.ExtraInfo = mod.IsInterceptionDisabled;
         mod.ExtraInfoAdditional = mock.ReturnValue;

         // Mark that original method execution should be skipped
         mod.IsInterceptionDisabled = true;

         if (mock.ReturnValue != null)
         {
            // If the recorded return object is identifiable as a 
            // pre-recorded return value, mark it as such
            ICanIdentifyAsMockObject mockIndicatable = 
               mock.ReturnValue as ICanIdentifyAsMockObject;

            if (mockIndicatable != null)
               mockIndicatable.IsMockReturnValue = true;
         }
      }
   }

   /// <summary>
   /// Re-enables original method execution
   /// </summary>      
   private void ReEnableMethodExecution(IInvocationContext context)
   {
      IModifiableType mod = (context.Target as IModifiableType);
      mod.IsInterceptionDisabled = Convert.ToBoolean(context.ExtraInfo);
      mod.ExtraInfoAdditional = null;
      context.ExtraInfo = null;
   }

   /// <summary>
   /// Records param and return values to MockObjectRepository for later playback
   /// </summary>
   private void RecordParametersToMock(IInvocationContext context, Type t, 
      RecordParametersAttribute attr, object returnValue)
   {
      MockObjectsForClass mockObjects = t.RetrieveMockObjectsForClass();
      if (!attr.OnlyWhenRecordingMocks || mockObjects.MockRecordingEnabled)
      {
         StoredParametersAndReturnValue found = 
            t.RecordMockObjectReturnValue(
               context.TargetMethod.Name, 
               context.Arguments, 
               returnValue);
            
         found.PackageMockForStorage(true, true);
      }
   }

   private void Execute(
      IInvocationContext context, CutpointType when, object returnValue)
   {
      object thisObject = context.Target;
      Type t = thisObject.GetType();
      RecordParametersAttribute[] attrs = 
         context.TargetMethod.ReadAttribute<RecordParametersAttribute>();

      DoNotMockMeAttribute[] attrsDoNotMock = 
         context.TargetMethod.ReadAttribute<DoNotMockMeAttribute>();

      bool classIsMockable = t.GetClassIsMockable();

      // Determine if the current method call is mockable

      if (classIsMockable || (attrs != null && attrs.Length > 0))
      {
         bool noMock = false;

         // Check the method for the DoNotMockMe attribute
         if (attrsDoNotMock != null && attrsDoNotMock.Length > 0)
         {
            foreach (DoNotMockMeAttribute doNotMock in attrsDoNotMock)
            {
               if (doNotMock.DoNotMock)
               {
                  noMock = true;
                  break;
               }
            }
         }

         RecordParametersAttribute attr;
         int count = classIsMockable ? 1 : 0;
         int total = (attrs == null) ? 0 : attrs.Length;

         for (int n = 0; n < total + count; n++)
         {
            // If the CLASS is marked with the ClassMethodsMockable attribute,
            // consider the method for recording/playback...
            attr = (classIsMockable && n == total) ? RecordParamForMocking : attrs[n];

            // unless the method is marked with the DoNotMockMe attribute
            if (!noMock)
            {
               // Replaying mock objects?  If so, set the
               // return value to the mock object and mark the 
               // context to skip execution of the original method
               if (when == CutpointType.Before && 
                  attr.AllowMockReplay && 
                  MockObjectRepository.MockReplayEnabled)
               {
                  UseMockReturnValue(context, t);
               }

               // Replaying mock objects?  Afterwards, re-enable execution of 
               // the original method
               if (when == CutpointType.After && 
                  attr.AllowMockReplay && 
                  MockObjectRepository.MockReplayEnabled && 
                  context.ExtraInfo != null)
               {
                  ReEnableMethodExecution(context);
               }

               // Recording parameters?
               if (when == CutpointType.After && 
                  (!attr.OnlyWhenRecordingMocks || 
                   MockObjectRepository.MockRecordingEnabled))
               {
                  RecordParametersToMock(context, t, attr, returnValue);
               }
            }

            MethodInfo methodBefore = attr.GetMethodBefore(t);
            MethodInfo methodAfter = attr.GetMethodAfter(t);

            // Optionally execute programmer-specified before and after methods 
            // when the method is called
            if (when == CutpointType.Before)
            {
               if (methodBefore != null)
                  methodBefore.Invoke(thisObject, new object[] { context });
            }
            else
            {
               if (methodAfter != null)
                  methodAfter.Invoke(thisObject, new object[] { context, returnValue });
            }
         }
      }
   }

   public override void AfterInvoke(IInvocationContext context, object returnValue)
   {
      Execute(context, CutpointType.After, returnValue);
   }

   public override void BeforeInvoke(IInvocationContext context)
   {
      Execute(context, CutpointType.Before, null);
   }
}

附录/杂项信息

需要以下类级别属性来启用 AOP 后置织造,并将类中的所有方法标记为可模拟:

  • AOPEnabled - 在装饰的类中启用 AOP 后置构建织造。
  • ClassMethodsMockable - 启用为装饰类定义的方法录制和回放模拟对象。

最后一个属性允许排除特定方法不进行模拟录制和回放(当类被标记为 ClassMethodsMockable 时)。

  • DoNotMockMe - 不允许为装饰方法进行模拟对象录制和回放。

AOPDemo.MockObjectDemo 和录制/回放

AOPDemo.MockObjectDemo 项目演示了返回值的录制和回放。具体来说,FindLogin 方法在接收任何用户名和密码时,都会返回一个随机生成的名字/姓氏。在录制模式下,将存储随机生成的姓名返回值,以及调用 FindLogin 方法时使用的参数值。在回放录制值时,您会注意到您*不会*获得随机姓名,而是会获得与给定用户名/密码参数组合关联的先前录制返回值。

Example

Example

Example

Example

解决方案内容/演示应用程序

VS2008 解决方案包含以下项目和演示应用程序:

  • AOPLibrary - AOP 相关库 - 包含本文讨论的功能的实现。
  • BrainTechLLC.Core - 我在多个应用程序中使用的通用函数。
  • AOPClasses - 演示应用程序中使用的 AOP 启用类。
  • LinFu.AOP.Weavers.Cecil, LinFu.AOP.CecilExtensions, PostWeaver, Simple.IoC.VS2008, Simple.IoC.Loaders.VS2008 - LinFu 项目。
  • AOPDemo.PropertyChangeTracking - 一个 Windows 应用程序,演示属性更改跟踪以及属性回滚到先前状态/先前值。
  • AOPDemo.PropertyRestrictionsAndAssertions - 一个 Windows 应用程序,演示限制属性更改和断言方法前后条件。
  • AOPDemo.Other - 演示计数方法调用和方法执行计时。
  • AOPDemo.MockObjectDemo - 演示参数和返回值的录制,以及基于方法参数的返回值的回放。
  • AOPDemo.Combined - 此应用程序包含上面列出的所有演示功能。

希望您发现本文有用和/或有趣!欢迎所有评论/建议。

电子邮件地址:owen@binarynorthwest.com。网站:BrainTechLLC

历史

  • 2008 年 9 月 4 日 - 文章发布。
© . All rights reserved.