Skip to main content

Stubbing Static Methods with PostSharp

TypeMock uses the Profiler API to allow mocking, stubbing, etc. of classes used by code under test. It has the ability to handle sealed classes, static classes, non-virtual methods, and other troublesome-yet-oft-encountered scenarios in the world of unit testing. Other frameworks rely on proxies to intercept method calls, limiting them to be able to only fake virtual, abstract, and interface members. They also rely on dependecy injection to place the proxies as the concrete implementation of calls to the abstracted interface members.

Anyone working with a legacy codebase is bound to run into static method calls (especially in the data access layer), dependencies on concrete types with non-virtual methods, and sealed class dependencies (HttpContext anyone?). The only way to unit test this without refactoring is with TypeMock. I've never used TypeMock, and I'm sure it's a great product, but it's not free. I decided to spike some code to see if I could solve the problem of mocking static method calls through AOP with PostSharp. What I came up with was a pretty ugly solution with limited applicability that will probably help no one. Let's take a look!

The example codebase is trivial: we have a PersonGetCommand representing the business logic we want to test and a PersonDao class with a static GetPerson method representing a method we want to stub. GetPerson returns, you guessed it, a Person.

To intercept calls to the static method, we're going to place a MockableAttribute on the method. It looks like this:



[Mockable]
public static Person GetPerson(int personId)
{
return new Person { Id = personId, Name = String.Format("Person {0}", personId) };
}


The code for the attribute talks to our 'mock framework'. It looks like this:



[Serializable]
public class MockableAttribute : OnMethodInvocationAspect
{
public override void OnInvocation(MethodInvocationEventArgs eventArgs)
{
MethodInfo invokedMethod = eventArgs.Delegate.Method;

// If this gets called during recording, we have to register the expectations
if (Expect.IsInRecordMode)
{
Expect.RegisterExpectedCall(invokedMethod);
Expect.RegisterExpectedArguments(invokedMethod, eventArgs.GetArgumentArray());
Expect.RegisterExpectedReturnValue(invokedMethod, Expect.CurrentlyExpectedReturnValue);
}
// If not, we need to check to see if we're mocking the method
else
{
// If it is mocked, we intercept the call
if (Expect.MethodIsMocked(invokedMethod))
{
eventArgs.ReturnValue = Expect.GetExpectedReturnValue(invokedMethod);
}
// Otherwise, we invoke the method
else
{
eventArgs.ReturnValue = eventArgs.Delegate.Method.Invoke(eventArgs.Delegate.Target, eventArgs.GetArgumentArray());
}
}
}
}


To prove that the example codebase works correctly, we have this test:



[Test]
public void Will_Get_Person()
{
var expectedName = "Person 23";
var personId = 23;

var commandUnderTest = new PersonGetCommand { PersonId = personId };
var resultingPerson = commandUnderTest.Execute();

Assert.That(resultingPerson.Name == expectedName);
Assert.That(resultingPerson.Id == personId);
}


And this test shows our MockableAttribute (as well as our makeshift mock framework) in action:



[Test]
public void Will_Get_Person_From_Mocked_Call()
{
var ryan = new Person { Id = 11, Name = "Ryan" };

// Set the expectation for the dao call
Expect.Call(delegate { PersonDao.GetPerson(0); }, ryan);

// Note that we don't care about the person id argument
var commandUnderTest = new PersonGetCommand();
var resultingPerson = commandUnderTest.Execute();

Assert.That(resultingPerson == ryan);
}


Note that I chose a Rhino-like syntax for the expecations. Also note that we aren't really 'mocking' the call because we aren't verifying expectations or arguments. This could easily be done, however.

This is all dreadfully simple thanks to PostSharp. But is this approach ready for primetime? It's far from it, in fact, and here are the reasons:


  1. It only works for classes you control (and currently only for static methods). So if you're making a call to a static method in a third-party dll or a .NET framework class, this is useless.

  2. Requires the use of an attribute. It may only be metadata, but testing concerns shouldn't be leaking into application source.

  3. It modifies that actual assemblies being tested. Technically, this approach tests only a modified-for-test assembly, not the assembly that we want to release into production.

  4. The assembly under test requires a reference to PostSharp.Public.dll and PostSharp.Laos.dll.

  5. Performance could be poor. Code weaving adds overhead to the compilation process. Tests and the code they test are frequently compiled.

  6. PostSharp is tied to MSBuild. NAnt tasks running on a build server might not be able to execute tests using this mock framework. (See comments to this post from PostSharp creator Gael Fraiteur.)

  7. A bunch of other things I forgot.



Some of these concerns can be alleviated, and some cannot. I would like to prove this out soon (though I doubt I'll be able to):


  1. Interception can be done at the call site without modifying the called assembly.

  2. The code weaver can be provided with interception points (join points? advice points? I don't understand AOP terminology at all) without them being marked with attributes. This has to be true if we want to do (1) anyway. Unfortunately, it's not going to be easy (otherwise Laos would probably already do it).

  3. Nothing can be done about modifying the assemblies being tested; that's the whole point. We're weaving code to achieve interception.

  4. Nothing can be done about referencing PostSharp. Again, that's the point. Gael corrected me on this. We'll tackle this in the distant future.

  5. Overhead per-compilation isn't too bad. It's better than overhead per-test.

  6. There are ways to invoke PostSharp from the command line, so NAnt tasks the coupling to MSBuild can be overcome (Deleted not only for being obvious typo nonsense but also because Gael points out that there is already a NAnt task for PS 1.5). I don't really care about this right now (still true anyway).

  7. I sometimes remember things I previously forgot.



If anyone ever reads this post and is interested, I will find a way to provide the solution I used to spike this.

(Side thought: I wonder, if I were to start an open source project called 'SharpMock' as an alternative TypeMock, could I get a free version of TypeMock to aid in its development?)

Comments

Gael Fraiteur said…
James,

This is a good use of PostSharp! hope you'll have the force to complete the project!

Some precisions:

1. PostSharp is not bound to MSBuild. There is a command-line utility (from version 1.0) and an NAnt task (from version 1.5).

2. PostSharp Laos is less bound to custom attributes than you may think. You you design a DSL or XML file, you can 'easily' read it from inside PostSharp. See CompoundAspect for high-level use, or ICustomAttributeProvider for low-level.

3. Depending on your time budget (hours or days/weeks), you can develop a solution that does not require PostSharp to be referenced to your assemblies. But you should use the low-level APIes for that... and study MSIL. It's not as easy as with PostSharp Laos, but you are virtually unlimited.

You can ask questions on http://www.postsharp.org/forum if you have any question.

Good luck!

-gael
Morgan said…
the code is cut off when viewed with FireFox.
Ryan said…
@Morgan:

This should be fixed now.

@Gael

Thanks for the feedback! I will actually update this post soon per your comments. Also, you can expect to hear from me soon on your excellent support forums :)
Anonymous said…
Ryan,

Before starting the open source project have a look at this patent by TypeMock http://www.wipo.int/pctdb/en/wo.jsp?WO=2008038265

If the url doesn't show google typemock patent office.

Basically they locked the concept of isolating code for testing purposes. That's why the crazy prices.

I was about to start an open source project just like you but then I just realized that there is no way around it. Maybe I misunderstood the patent text so I might be wrong. Somebody should clarify that.

Patents are pretty bad for evolution in general.

Cheers,
Cosmin
Ryan said…
@Cosmin,

Thanks for the heads up. I might look into this more later, but for now I'm going to simply ignore it... I'm stating that plainly here for everyone to see. I will deal with this in more detail if it becomes a problem.

Intellectual property laws in general are bad for progress.
Unknown said…
Ryan,

Interesting use of PostSharp for this. I'd like you to stay on course, and see where it goes!

So if you'd like a Typemock Isolator license, feel free to contact me at gilz at typemock dot com. We give free licenses for open source projects. And you won't be violating any patent, I assure you. You can download the open source version (fully featured) here.

And do let me know when and how you continue. I'm interested in driving innovation more than protecting IP.

Gil Zilberfeld
Typemock

Popular posts from this blog

Migrating Hg Repos with hg-fast-export and Windows Subsystem for Linux

Introduction I prefer Mercurial (hg) to git . I don’t really have any reason for this preference - they both do the same thing, and the user experience for 90% of the use cases is the same. It probably comes from the conditions of the DVCS landscape when I started using these systems. Some of this may have been perception only, but it looked like this: GitHub didn’t have free private repos BitBucket did have free private repos BitBucket was very hg-friendly Joel Spolsky had an amazing tutorial that served as both a how-to for hg as well as a general intro to DVCS hg was much more Windows-friendly than git Since hg was written in python, I felt like extending it would be easier than doing so for git if I ever needed to (admittedly, this is a pretty ridiculous reason) hg felt like a more unified, “coherent” system than the very linux-y feeling git and its extensions (also pretty ridiculous) Where they differed, I liked the verbs hg used better than git’s counterparts ...

Enabling Globalization Invariant Mode for .NET Core App on Raspberry Pi Running LibreElec

I had an app I wanted to run on my Raspberry Pi 3 running LibreElec . In LibreElec you can install the dotnet core 2.2 runtime as an addon, and in Visual Studio you can compile for ARM processors with ‘Target Runtime’ set to ‘linux-arm’ in the publish profile. So, I published to a folder from VS using that profile, and I copied the output over to my RPi which had the dotnet runtime installed. I did a simple dotnet Whatever.dll to run the app (actually in this case, it was /storage/.kodi/addons/tools.dotnet-runtime/bin/dotnet Whatever.dll because of the way the addon is installed) and was met with this error: FailFast: Couldn't find a valid ICU package installed on the system. Set the configuration flag System.Globalization.Invariant to true if you want to run with no globalization support. at System.Environment.FailFast(System.String) at System.Globalization.GlobalizationMode.GetGlobalizationInvariantMode() at System.Globalization.GlobalizationMode..cctor() at Syste...