Skip to content
Tom van der Kleij edited this page Jun 8, 2015 · 1 revision

Extracting Setup(() => ...) calls

The first step in the process is to identify the properties or methods to mock. This is done by disassembling the method provided to Smock.Run to CIL instructions and finding all calls to `ISmocksContext.Setup(() => ...).

The argument to ISmocksContext.Setup is an expression tree that contains the method or property that is the target of the setup. When one calls ISmocksContext.Setup, the compiler emits instructions to generate the expression tree that represents the expression provided to Setup. When Smocks finds a call to ISmocksContext.Setup, it decompiles these instructions to reconstruct the expression tree.

Consider the following code:

Smock.Run(context =>
{
    context.Setup(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));

    Console.WriteLine(DateTime.Now);
}

Performing the setup extraction on this code yields an expression tree for () => DateTime.Now, without executing the Console.Writeline() call. The expression trees obtained in this step, can then be used to identify all properties or methods that are the target of a Setup call.

Running the method in a separate AppDomain

Smocks works by replacing all calls to targeted properties and methods with a call to an interceptor method. To do this, it generates modified replacement assemblies that it loads instead of the original assemblies. However, once an assembly has been loaded in an AppDomain in .NET, it cannot be unloaded or reloaded. This makes it impossible to modify an assembly that is already loaded. When the user calls Smock.Run(context => ...), the assembly containing the lambda to run, is the same assembly that contains the currently executing method. This means that at least one of the assemblies that Smocks should rewrite, is already loaded and cannot be changed.

To resolve this problem, Smocks starts a new AppDomain that does not have any assemblies loaded yet. It then installs an AppDomain.AssemblyResolve event handler and executes the lambda provided to Smock.Run. The AssemblyResolve event will be triggered for every assembly referenced by the executing lambda. The AssemblyResolve handler will then rewrite the referenced assemblies and load these assemblies instead of the original ones.

Rewriting calls to configured methods

As stated earlier, Smocks works by replacing all calls to properties or methods that have an associated Setup(() => ...) with a call to an interceptor method. Consider the following example:

Smock.Run(context =>
{
    context.Setup(() => string.Format("1 + 1 = {0}", 2)).Returns("1 + 1 = 3");
    Console.WriteLine(string.Format("1 + 1 = {0}", 2));
});

Smocks first extracts the Setup expression tree () => string.Format("1 + 1 = {0}", 2) to find that the method string.Format(string, object) is targeted. It then notices there's a call to this method in the Console.WriteLine() expression and replaces it with a call to a static interceptor method. After rewriting, the method roughly looks like this:

context.Setup(() => string.Format("1 + 1 = {0}", 2)).Returns("1 + 1 = 3");

// Get a MethodBase representing the target method.
MethodBase method = typeof(string).GetMethod("Format", 
    new [] { typeof(string), typeof(object)});

// Store the arguments of the method call.
object[] arguments = new object[2];
arguments[0] = "1 + 1 = {0}";
arguments[1] = 2;

// Intercept the method call and get a replacement return value.
string replacementReturnValue = Interceptor.Intercept<string>(arguments, method);

Console.WriteLine(replacementReturnValue);

The static Interceptor.Intercept method knows the setups configured for the targeted method. It checks if any setup matches the provided arguments. If there's a matching setup, any configured behaviour (returning a constant value, throwing an exception, etc.) for the setup will be applied by the interceptor. The interceptor also tracks all invocations or targeted methods, so that calls to Verify can confirm whether or not a configured setup was matched.

Serializing closures

Smocks runs rewritten assemblies in a separate AppDomain. Dealing with AppDomains can be rather tricky. AppDomains are fully separated worlds that don't share data, like separate processes. Microsoft has done a decent job at providing means of making communication between these separated worlds fairly easy, but there's a lot of remoting and serialization going on under the hood. This means that any data you want to pass between AppDomains must be serializable. This is the cause of an interesting problem. Consider the following example:

AppDomain domain = AppDomain.CreateDomain("Test");

string message = "Hello, world";

domain.DoCallBack(() =>
{
    Console.WriteLine(message);
});

Here we invoke the DoCallBack method, which runs a method in the targeted AppDomain, with a lambda that captures the variable message. Under the hood, C# generates a closure class containing a method for the body of the lambda and a field for the captured message variable. It then asks the AppDomain to execute the method on the closure class, serializing the closure class in the process. However, since generated closures are not serializable, an exception will be thrown:

Additional information: Type 'AppDomainSample.Program+<>c__DisplayClass1' in assembly 'AppDomainSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.

Even though a string is serializable, the class generated to wrap the string is not, resulting in this error.

Smocks resolves this problem by handling the serialization explicitly. Any Delegate passed to Smocks.Run is decomposed into the Method to run and the Target instance of the method. The target is then serialized to a structure that is serializable, such as a string. The serialized target and the method (which is serializable by default) are then transferred to the other AppDomain, where the original Delegate is reconstructed by deserializing the target and using it as the target for the method.

After invocation of the reconstructed Delegate, this process can be reversed to ensure any changes made to the deserialized target of the Delegate are transferred back to the original target: the target is serialized again and transferred back to the calling AppDomain. There, the target is deserialized onto the original target of the Delegate. This mechanism allows Smocks to support scenarios such as:

DateTime now = default(DateTime);

Smock.Run(context =>
{
    context.Setup(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));

    now = DateTime.Now;
});

// Outputs: "2000"
Console.WriteLine(now.Year);

It should be noted however, that this mechanism can only transfer variables that are serializable or inherit from MarshalByRefObject. Capturing variables in a Smocks.Run scope should therefore be used with caution.