Introduction
The presented library uses the Aspect Oriented Programming related functionality provided by the LinFu project (which itself uses Cecil.Mono) to address a multitude of cross-cutting concerns. These concerns include logging method calls, timing method execution, retrying method execution, restricting or preventing changes to property values, rolling back property changes to a previous state, ensuring single thread execution of methods, asserting global conditions pre- and post-method-execution, allowing methods and properties to be observable, and (last but not least) recording method parameters and playing back method return values.
Background: Why?
I currently work with a small IT team that believes in writing unit tests for code as the code is written. When I first joined the team, I dutifully wrote unit tests for each module I worked on. Soon, though, I began to notice that the unit tests were rarely run, and several times when I opened the solution containing all unit tests, it would not compile due to errors. Thinking this was a problem with developers not updating their unit tests as they modified dependent code, I went about fixing the compile-time errors. When I went to run the unit tests, almost all of the tests failed... What was up?
The majority of the unit tests relied on retrieving test data from the database. When the test data was removed or modified (for instance, when the production database was copied to our test environment), unit tests would fail. Therefore, since nobody could really *use* the unit tests more than a handful of times, developers had little motivation to update their test code as features were added, changed, or removed.
Possible Solutions?
I came up with several solutions, all of which are fairly well documented on the web:
- One solution involved restoring a database backup before each test. However, unit tests already were taking long enough to run - and even after stripping down our database (normally 3GB) to create a smaller test database (200-300MB), each database restore operation still took a while to complete.
- I explored what it would take to write code that inserted the appropriate data into the database if it did not exist. However, most of the unit tests relied on a multitude of data in many different tables, and furthermore relied on specific flags being set or not set. Writing code to generate the appropriate test data for each unit test was simply impractical.
- As another solution, I explored the idea of abstracting the way in which we retrieved our data so that our data source could be swapped transparently. However, implementing this (or a similar solution) would mean rewriting our data access gateway layer. I was certainly not thrilled about the prospect of rewriting such a critical (and large) component that was already functioning just fine in our production environment. However, the concept of modifying the source from which our gateway retrieved data was an interesting one...
- I finally stumbled across RhinoMock, NMock, and EasyMock - frameworks built to provide testable objects for unit testing. However, using these frameworks would involve modifying unit test code - something that would be difficult to convince the developers was worthwhile. Plus, if a developer accidentally called a gateway method, we could still run into the situation where expected data could not be retrieved from the database, resulting in likely-still-functional code failing to pass a unit test.
Design by Ideals
I finally decided that, to move forward with any solution, I needed to write down what I really wanted to do. I came up with the following list of requirements:
- I didn't want to touch / modify existing gateway code, and I didn't want my solution to alter production code in any way, shape, or form.
- I wanted to be able to record values returned from gateway methods. This would involve the following:
- During an initial execution of the unit test (when the proper test data is in the database), intercept all calls made to any gateway method.
- Store the parameters used to call the gateway method, and then store any value returned by the gateway method.
- Save all recorded parameters and return values used during execution of the unit test to disk.
- I wanted to be able to replay pre-recorded return values at a later time, bypassing execution of the gateway methods. To do this, I would need to:
- Intercept all calls made to any gateway method during execution of unit tests.
- When a gateway method was called during subsequent unit test runs, bypass the method call. Instead, the gateway method would return pre-recorded data - data that was in the database at the time the unit test was written.
- Addressing the issue of a unit test possibly getting back stale (unrealistic/old) data, I also wanted the capability of re-recording returned data from the database and then playing back the new data to the unit test.
Recording Parameters and Return Values
When a gateway method is called, the parameter values used and the method's return value should be recorded for later use, as shown in the diagram below:
Playing Back Pre-recorded Return Values
When a gateway method is called during playback, the return value should be retrieved by matching method parameter values, and the original gateway method should be skipped altogether, as shown in the diagram below:
The next step was to determine how I could dynamically modify the program flow to mimic either of the execution paths shown in the diagrams above...
Aspect Oriented Programming (AOP)
Cruising through CodeProject, online books, and programming sites, I stumbled across the concept of Aspect Oriented Programming (AOP). Crosscuts, advice, join points, pointcuts, etc., etc., etc... the terminology was all nice and good, but what was important was that, by using an Aspect-Oriented Programming concept called 'weaving', I could execute code before a method call, after a method call, or replace a method call altogether in our test builds, with minimal modification to our original source code. Furthermore, by *not* weaving the production build, I could ensure that the existing code would be executed in an unmodified and unmolested state in our production environment.
Next step - In my usual manner, I promised myself I would read a few academic papers on the subject... later... and then set about writing a library that used AOP to solve our problem. I chose to build atop the LinFu AOP library (which uses Cecil.Mono), and I must give credit to both projects - it would have been a heck of a lot more difficult to do what I wanted to do without having a lot of the dirty work already done for me.
Postweaver / Post-Build Weaving
LinFu uses a process called Weaving to allow programmer-defined code to be executed before, after, and/or instead of a method call. To do this, IL instructions are injected into a compiled assembly, wrapping each original method. The instructions that wrap the original method calls allow conditional branching to programmer-defined code to occur before method execution, after method execution, and/or instead of the original method's execution.
Example Use
Before going further, I'd like to show a few code snippets that demonstrate the various features of the library presented in this article.
The first example shows a method called MakeDeposit
(think: bank transaction). By decorating the method with the SingleThreadExecution
attribute, the method is guaranteed not to be executed concurrently on multiple threads. By decorating the method with the ObservableMethod
attribute, observers can be registered and notified whenever the MakeDeposit
method is called:
[SingleThreadExecution("AccountActivity")]
[ObservableMethod]
public double MakeDeposit(double amount)
{
AccountBalance += amount;
return AccountBalance;
}
The method WithdrawMoney
is presented below. The AssertBeforeAfter
attribute denotes that the PreCheckWithdrawl
method will be called before WithdrawMoney
is executed. If the method returns false
or throws an exception, an exception can be raised or the method execution can be skipped. The PostCheckWithdrawl
attribute denotes that the PostCheckWithdrawl
method should be called after execution to ensure that nothing sneaky went on during the withdrawal. The SingleThreadExecution
attribute ensures that:
WithdrawMoney
is not executed concurrently on multiple threads, and - that
WithdrawMoney
is not executed concurrently with any other method that is also marked with the [SingleThreadExecution("AccountActivity")]
attribute (i.e. the MakeDeposit
method, presented above):
[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;
}
After depositing $100, attempting to withdraw $60 twice would then result in output similar to what is shown below (the example below is from the demo app):
In the example below, the property SSN
stores a customer's social security number (probably not the best idea...). Because the property is marked with the RestrictPropertyChange
attribute, the ValidateSSN
method will be called before the property's value is changed. If the ValidateSSN
method returns false
, the property change will not be allowed - the code within set {...}
will be bypassed completely. Furthermore, since the Customer
class implements ISupportsPropertyChangeRestrictedNotify
, the PropertyChangeRestricted
event will be raised if the property change is restricted. (If the class does not implement ISupportsPropertyChangeRestrictedNotify
, the change is blocked without notification.) Next, the PropertyChangedAction
attribute specifies that some action should be taken whenever the property value is successfully changed. In the example below, the property AccountOwnerSSN
of the the BankAccount
class instance stored in the property Account
will be automatically set to reflect the new SSN value.
[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;
}
}
In the above example, the class Customer
has several properties marked with the RecordPropertyChanged
attribute. This allows us to use the library's built-in ChangeTracker
to do the following:
Customer customer = new Customer()
{
FirstName = "Fred",
LastName = "Flintstone",
SSN = "111992222"
};
customer.ChangeTracker.ResetPropertyChanges();
customer.FirstName = "Freddy";
customer.SSN = "111992223";
customer.ChangeTracker.RevertChangesToLastReset(customer);
The source code and the solution includes several demo apps which expand upon the code snippets presented above and demonstrate various features of the library.
Implementing AOP - Standard Examples
To familiarize myself with AOP basics, I started by implementing two of the most boring (but still useful) features I could think of - keeping a count of times a method is called, and logging. Specifically, with logging, my goal was to write out a log entry both before and after a specific method was called. The only change I wanted to have to make to my existing code was to add an attribute to the method I wanted logged. Example:
[Logable(LogSeverity.Information, "Before MakeDeposit", "After MakeDeposit")]
public double MakeDeposit(double amount)
{
...
}
AOP Using the LinFu Framework
Using the LinFu framework, classes that implement IAroundInvoke
are used to hook into woven assemblies. At runtime, the IAroundInvoke
methods BeforeInvoke
and AfterInvoke
are called before and after the original method execution.
An instance of an IInvocationContext
-implementing class is used to pass the context information to the BeforeInvoke
and AfterInvoke
methods. The IInvocationContext
parameter contains information such as the method name being called, the parameter values being passed to the method, and other information about the calling context. To weave an assembly and enable this AOP/method-related black magic, LinFu/Cecil's Postweaver is used; as an example, the post-build event "postweaver.exe $(TargetPath)" is run after building the project AOPClasses in the provided solution.
Perhaps, the easiest way to see how this all works is to present a well-commented example: The LogWrapper
, presented below, is a class used to write out log entries before and after method execution using an implementation of LinFu's IAroundInvoke
interface:
public class LogWrapper : AroundMethodBase, IAroundInvoke
{
public override void BeforeInvoke(IInvocationContext context)
{
Execute(context, null, CutpointType.Before);
}
public override void AfterInvoke(
IInvocationContext context, object returnValue)
{
Execute(context, returnValue, CutpointType.After);
}
private void Execute(
IInvocationContext context, object returnValue, CutpointType when)
{
object thisObject = context.Target;
LogableAttribute[] attrs =
context.TargetMethod.ReadAttribute<LogableAttribute>();
attrs.ForEachAttribute(delegate(LogableAttribute attr)
{
if (when == CutpointType.Before &&
!string.IsNullOrEmpty(attr.MessageType))
{
GlobalLogger.Log(
attr.MessageType,
String.Format(attr.LogMessage, context.Arguments),
attr.LogLevel);
}
else if (when == CutpointType.After &&
!string.IsNullOrEmpty(attr.MessageTypeAfter))
{
GlobalLogger.Log(
attr.MessageTypeAfter,
String.Format(attr.LogMessageAfter, returnValue),
attr.LogLevelAfter);
}
});
}
}
Something a Little More Challenging...
This was all fairly easy (and very cool), so I decided to try my hand at implementing a RestrictPropertyChange
attribute that, when attached to a property, can skip execution of the "set
" code used to change the property value - if the value does not meet certain user-defined criteria. Also, I wanted notification of the restricted change, either in the form of an exception being thrown, or via a raised event:
To do this, I needed a way to:
- Specify (at runtime) that execution of the original method should be skipped.
- Skip execution of the original method, without affecting concurrent execution of other methods defined by the class.
To do this, I had to modify the LinFu- and Cecil-based code that performed post-build weaving.
- I added three properties to the definition of (and post-build creation of)
IModifiableType
: two object properties to store misc./additional pieces of data, and a boolean property, IsInterceptionDisabled
, to denote that the original method call should be skipped. - It was necessary to modify the LinFu/Cecil-based weaver to generate the appropriate IL to check the value of
IsInterceptionDisabled
and possibly skip execution of the original method. The IL generated by the following code checks the IsInterceptionDisabled
flag (_isDisabled
, below) and branches if true
, skipping execution of the original method call:
instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _isDisabled));
instructions.Enqueue(IL.Create(OpCodes.Brfalse, skipToEnd));
instructions.Enqueue(IL.Create(OpCodes.Br, JumpForDone));
instructions.Enqueue(skipToEnd);
After making these changes, we can mark that execution of the original method should be skipped in BeforeInvoke
by setting the IsInterceptionDisable
property to true
prior to the original method call:
IModifiableType mod = (context.Target as IModifiableType);
mod.IsInterceptionDisabled = true;
The class that implements IAroundInvoke
and executes or skips set_Property
based on a value, is shown below:
public class RestrictPropertyChangeWrapper : AroundMethodBase, IAroundInvoke
{
public override void BeforeInvoke(IInvocationContext context)
{
Execute(context, CutpointType.Before);
}
public override void AfterInvoke(IInvocationContext context, object returnValue)
{
if (context.ExtraInfo != null)
RestoreIsInterceptionDisabledFlag(context);
}
private void Execute(IInvocationContext context, CutpointType when)
{
object thisObject = context.Target;
Type t = thisObject.GetType();
string propInfoName = context.TargetMethod.Name.Substring(4);
PropInfo propInfo = t.GetPropInfo(propInfoName);
RestrictPropertyChangeAttribute[] attrs =
propInfo.GetAttributes<RestrictPropertyChangeAttribute>();
if (attrs != null && attrs.Length > 0)
{
object oldValue = propInfo.GetValue(thisObject);
object newValue = context.Arguments[0];
for (int n = 0; n < attrs.Length; n++)
{
RestrictPropertyChangeAttribute attr = attrs[n];
Exception ex = attr.IsRestricted(
thisObject, t, propInfo.Name, oldValue, newValue, context);
if (ex != null)
{
ISupportsPropertyChangeRestrictedNotify notify =
thisObject as ISupportsPropertyChangeRestrictedNotify;
if (notify != null)
notify.NotifyPropertyChangeRestricted(
context, propInfo.PropertyInfo, oldValue, newValue, ex);
SetInterceptionDisabledFlag(context, true);
if (attr.ThrowOnException)
throw ex;
}
}
}
}
private void RestoreIsInterceptionDisabledFlag(IInvocationContext context)
{
IModifiableType mod = context.Target as IModifiableType;
mod.IsInterceptionDisabled = Convert.ToBoolean(context.ExtraInfo);
context.ExtraInfo = null;
}
private void SetInterceptionDisabledFlag(
IInvocationContext context, bool value)
{
IModifiableType mod = (context.Target as IModifiableType);
context.ExtraInfo = mod.IsInterceptionDisabled;
mod.IsInterceptionDisabled = true;
}
}
After seeing LinFu and Cecil's power in action, I went on a bit of a rampage, and implemented the following attributes and features I thought might be useful:
CountCalls
- keeps a tally of how many times the decorated method has been called. Logable
- adds a log entry before and/or after each call to the decorated method. Retry
- retries execution a certain number of times if an exception is thrown during execution of the decorated method. TimeMethod
- times how long it takes for the decorated method to execute. SingleThreadExecution
- ensures that the method is not executed more than one thread at any given time. IfCase
- executes code before method execution only when certain conditions are met. AssertBeforeAfter
- checks that specific conditions are either true or false, both before and after method execution. ObservableMethod
- observers can be registered dynamically. When the decorated method is called, all registered observers are notified. RecordParameters
- both method input parameters and the method return value is recorded when the decorated method is called. ObservableProperty
- observers can be registered dynamically. When the decorated property is set, all registered observers are notified. LogPropertyChanged
- adds a log entry with the old and new property value whenever the decorated property value is changed. PropertyChangedAction
- executes a developer-defined method whenever the decorated property value changes. RecordPropertyChange
- records before and after values when the decorated property value changes. RestrictPropertyChange
- checks the before/after value before the decorated property value changes, and skips/bypasses changing the property value if the defined conditions are not met.
Performance Overhead?
Yep, there is a performance overhead associated with executing woven assemblies; all woven method calls (including property sets and gets) end up doing extra work, both before and after original method execution. To help limit this overhead to only classes that absolutely need these AOP-related capabilities, I defined the AOPEnabled
attribute and modified Postweaver to only weave classes marked with this attribute:
[AOPEnabled]
public class BankAccount : ChangeTrackingBaseClass<BankAccount>
{
...
}
Addressing the Original Issue: Unit Tests
My interest in AOP started with the unit test/data inconsistency issue I encountered at work (discussed in the Background section - up top). Basically, unit tests quickly became useless because the data unit tests relied upon frequently disappeared from the database or was modified. I wanted a way to transparently record method return values (in recording mode) and play back the return values (in playback mode). Using LinFu/Cecil, and AOP concepts gave me all the necessary tools - I could now:
- intercept methods before execution and record the parameters being passed in,
- observe the method's return value before returning execution to the caller, and
- choose whether or not to execute the original method.
Here's how it ended up working:
Recording Mode
AOP.EnableAOP(MockMode.Recording);
typeof(MyClass).EnableMockRecording(false);
typeof(MyClass).EnableMockRecording(true);
In 'recording' mode (for methods marked as 'Mock-able'):
- When a method is called, the parameters passed in to the method and the method's return value are recorded.
- The return value is stored in a lookup table and can be retrieved using the method's name and original parameter values.
- When recording is done, method names, parameters, and return values are serialized out to a file for later retrieval.
Playback Mode
AOP.EnableAOP(MockMode.Playback);
MockObjectRepository.AddMockObjectsFromFile("RecordedRetVals.dat");
In 'playback' mode (for methods marked as 'Mock-able'):
- Previously recorded method names, parameters, and return values can be loaded and deserialized from file and added to a lookup table for quick retrieval.
- When a mock-able method is called, the lookup table is used to find a previously-recorded return value associated with the method name and the same parameters used to make the original call.
- If there is a matching return value (same class type, same method name, same parameters), execution of the original method is skipped, and the previously stored return value is used as the method call's return value.
In order to substitute a mock return value for a real one, bypassing the original method call, I had to add the following code to the LinFu/Cecil Postweaver:
instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _extraInfo));
instructions.Enqueue(IL.Create(OpCodes.Brfalse, skipToEnd));
instructions.Enqueue(IL.Create(OpCodes.Ldarg_0));
instructions.Enqueue(IL.Create(OpCodes.Isinst, _modifiableType));
instructions.Enqueue(IL.Create(OpCodes.Callvirt, _extraInfo));
instructions.Enqueue(IL.Create(OpCodes.Br, JumpForDone));
instructions.Enqueue(skipToEnd);
MockObjectRepository: Storing Parameter and Return Values
Consider the following code:
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");
Each call to FindCustomerUsingPersonalInfo
passes in different parameter values, and each call (most likely) returns a different list of customers, depending on the parameter values passed in. To handle variation of return values by parameter values, I use instances of the class StoredParametersAndReturnValue
to store hash codes that identify parameter values passed into a method call and the value that was returned from the method, given the specified parameters. The class also defines two methods, PackageMockForStorage
and UnpackageMockFromStorage
, that can be used to serialize and deserialize return values and parameter hash values to/from a compressed byte array.
In order to locate return values for a specific class type and method, I chose to store parameters and return values using the following data structures:
At the most basic level is the StoredParametersAndReturnValue
class, discussed above. At a level up from individual stored parameters and return values, instances of the class MockObjectsForMethod
are used to store the parameter hash and return values associated with a single method. At the next level up, MockObjectsForClass
is used to store a collection of MockObjectsForMethod
for a single class type. Finally, a single instance of MockObjectRepository
is used to store a collection of MockObjectsForClass
objects. Storing return values by class type and method name lets us quickly drill down into all recorded return values associated with a specific class and method.
How it's Implemented: Recording and Retrieving Return Values
The method RecordMockObjectReturnValue
of class MockObjectRepository
is used to record a return value associated with a specific class type and method name, when passed a specific set of parameter values:
public static StoredParametersAndReturnValue RecordMockObjectReturnValue(
this Type t, string methodName, object[] parameters, object returnValue)
{
MockObjectsForClass mockObjects = RetrieveMockObjectsForClass(t);
List<int> paramHash = GetParametersHash(parameters);
int[] paramsHashArray = paramHash.ToArray();
MockObjectsForMethod methodMocks =
mockObjects.GetMockObjectsForMethod(methodName);
StoredParametersAndReturnValue found =
methodMocks.LookupByParameters.FindItem(paramsHashArray);
if (found == null)
found = new StoredParametersAndReturnValue();
else
methodMocks.LookupByParameters.RemoveTerminatingItem(paramsHashArray);
found.ListOfParameterHash = paramHash;
found.Parameters = new List<object>(parameters);
found.ReturnValue = returnValue;
methodMocks.LookupByParameters.AddTerminatingItem(found, paramsHashArray);
return found;
}
The method StoredParametersAndReturnValue
of the class MockObjectRepository
is used to find a previously-recorded return value associated with a specific class type and method name, given specific parameter values:
public static StoredParametersAndReturnValue GetMockObject(
this Type t,
string methodName,
params object[] parameters)
{
MockObjectsForClass mockObjects = RetrieveMockObjectsForClass(t);
List<int> paramHash = GetParametersHash(parameters);
MockObjectsForMethod methodMocks =
mockObjects.GetMockObjectsForMethod(methodName);
StoredParametersAndReturnValue found =
methodMocks.LookupByParameters.FindItem(paramHash.ToArray());
if (found != null && found.IsPacked)
found.UnpackageMockFromStorage(true, true);
return found;
}
Bringing all of this functionality together, the class RecordParametersWrapper
implements IAroundInvoke
, records and/or plays back return values, and has the capability of skipping the original method call while transparently providing a pre-recorded return value to the caller:
public class RecordParametersWrapper : AroundMethodBase, IAroundInvoke
{
private static readonly RecordParametersAttribute RecordParamForMocking =
new RecordParametersAttribute(true, true);
private void UseMockReturnValue(IInvocationContext context, Type t)
{
StoredParametersAndReturnValue mock =
t.GetMockObject(context.TargetMethod.Name, context.Arguments);
if (mock != null)
{
IModifiableType mod = (context.Target as IModifiableType);
context.ExtraInfo = mod.IsInterceptionDisabled;
mod.ExtraInfoAdditional = mock.ReturnValue;
mod.IsInterceptionDisabled = true;
if (mock.ReturnValue != null)
{
ICanIdentifyAsMockObject mockIndicatable =
mock.ReturnValue as ICanIdentifyAsMockObject;
if (mockIndicatable != null)
mockIndicatable.IsMockReturnValue = true;
}
}
}
private void ReEnableMethodExecution(IInvocationContext context)
{
IModifiableType mod = (context.Target as IModifiableType);
mod.IsInterceptionDisabled = Convert.ToBoolean(context.ExtraInfo);
mod.ExtraInfoAdditional = null;
context.ExtraInfo = null;
}
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();
if (classIsMockable || (attrs != null && attrs.Length > 0))
{
bool noMock = false;
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++)
{
attr = (classIsMockable && n == total) ? RecordParamForMocking : attrs[n];
if (!noMock)
{
if (when == CutpointType.Before &&
attr.AllowMockReplay &&
MockObjectRepository.MockReplayEnabled)
{
UseMockReturnValue(context, t);
}
if (when == CutpointType.After &&
attr.AllowMockReplay &&
MockObjectRepository.MockReplayEnabled &&
context.ExtraInfo != null)
{
ReEnableMethodExecution(context);
}
if (when == CutpointType.After &&
(!attr.OnlyWhenRecordingMocks ||
MockObjectRepository.MockRecordingEnabled))
{
RecordParametersToMock(context, t, attr, returnValue);
}
}
MethodInfo methodBefore = attr.GetMethodBefore(t);
MethodInfo methodAfter = attr.GetMethodAfter(t);
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);
}
}
Addendum / Misc. Info
The following class-level attributes are needed to enable AOP post-weaving and to mark all methods in a class as mock-able:
AOPEnabled
- enables AOP post-build weaving in the decorated class. ClassMethodsMockable
- enables the recording and replaying of mock objects for methods defined by the decorated class.
One final attribute allows specific methods to be excluded from mock recording and playback (when the class is marked with the ClassMethodsMockable
attribute):
DoNotMockMe
- Do not allow mock object recording and playback for the decorated method.
AOPDemo.MockObjectDemo and Record/Replay
The AOPDemo.MockObjectDemo project demonstrates recording and playback of return values. Specifically, the method FindLogin
returns a randomly-generated first/last name when passed any username and password. In recording mode, the randomly-generated first/last name return values are stored, along with the parameter values used when calling the FindLogin
method. When playing back recorded values, you'll note that you do not get back random names, but instead, are presented with the previously-recorded return values associated with the given username/password parameter combination.
Solution Contents / Demo Applications
The VS2008 solution contains the following projects and demo applications:
- AOPLibrary - AOP-related library - contains implementation of the features discussed in this article.
- BrainTechLLC.Core - common functions I use across multiple applications.
- AOPClasses - AOP-enabled classes used in the demo applications.
- LinFu.AOP.Weavers.Cecil, LinFu.AOP.CecilExtensions, PostWeaver, Simple.IoC.VS2008, Simple.IoC.Loaders.VS2008 - LinFu projects.
- AOPDemo.PropertyChangeTracking - a Windows app that demonstrates property change tracking, and rollback of properties to a previous state / prior values.
- AOPDemo.PropertyRestrictionsAndAssertions - a Windows app that demonstrates restricting property changes, and asserting method pre- and post-conditions.
- AOPDemo.Other - demonstrates counting calls made to a method and method execution timing.
- AOPDemo.MockObjectDemo - demonstrates recording of parameters and return values, and playback of return values based on method parameters.
- AOPDemo.Combined - this application includes all of the demo features listed above.
Hope you found the article useful and/or interesting! All comments/suggestions are welcome.
Email address: owen@binarynorthwest.com. Website: BrainTechLLC.
History
- Sept 4, 2008 - Article posted.