piątek, 7 sierpnia 2009

How to test generic class/methods with Rhino mocks

Rhino mocks is my isolation framework of choice and as you my notice from my earlier post I use generics as my Inversion of Control tool. Problem is that when you use Rhino Mocks you cannot mock constructor (right now to my knowledge only TypeMock Isolator can do this). The reason for this is that Rhino Mocks generates mocks at runtime, and for testing methods that use new() constraint you need to know the mock type at compile time.
So the only way to solve this seems to be creation of mock classes by hand. But then shortly I realized that all of them seems to look very similar.
So…

Here is how I solved this:
First I created t4 file called Mocks.tt which I attach to my test project.

It’s almost unreadable, so don’t be scared – I’m not t4 expert, it’s my first experiment :-)

This file is:

  • generating MockAttribute class
  • searching for each class in project (it belongs to) that have such attribute and are partial and then creates mock methods for each interface this class implements and for default constructor.

To create such mock type I must first write something like this:

[Mock]

public partial class MockAskEmailWindow : IQuestion

{

}

Then click “Run Custom Tool”

run custom tool

And because IQuestion is defined like this

public interface IQuestion

{

    string Ask(string question);

}

in file generated by t4 script I have following code

public partial class MockAskEmailWindow

{

    private readonly MockAskEmailWindow mock;

    public bool Created;

    public MockAskEmailWindow Object;

 

    public MockAskEmailWindow()

    {

        if (Service<Queue<MockAskEmailWindow>>.Value != null)

        {

            mock = Service<Queue<MockAskEmailWindow>>.Value.Dequeue();

            mock.Created = true;

            mock.Object = this;

        }

    }

    public virtual string Ask( string question)

    {

        return mock.Ask( question);

    }

}

Now if we have following method to test:

public string AskForNewUserEmail<TQuestion>() where TQuestion : IQuestion, new()

{

    throw new NotSupportedException();

}

We write test like this:

[Test]

public void ShowUsermNewUserEmailRequestAndRetrieveEmail()

{

 

    var ask = MockRepository.GenerateMock<MockAskEmailWindow>();

 

    ask.Expect(a => a.Ask("Plase type email of the user you want to create")).Return("test@test.pl");

 

    using (Service.CreateQueue(ask))

        Assert.AreEqual("test@test.pl", AskForNewUserEmail<MockAskEmailWindow>());

    ask.VerifyAllExpectations();

}

And to make test pass we change our method to

public string AskForNewUserEmail<TQuestion>() where TQuestion : IQuestion, new()

{

    return new TQuestion().Ask("Plase type email of the user you want to create");

}

Seems, simple :-)
The only unknown class that I used here is Service<> so here is it’s very simple code

public class Service<T> : IDisposable where T : class

{

    private readonly T parent;

    [ThreadStatic] private static T current;

 

    public Service(T service)

    {

        parent = Value;

        Value = service;

    }

 

    public static T Value

    {

        get { return current;}

        private set {current = value;}

    }

 

    public void Dispose()

    {

        Value = parent;

    }

}

 

public static class Service

{

    public static Service<Queue<T>> CreateQueue<T>(T value, params T[] values)

    {

        var queue = new Queue<T>(values.Length + 1);

        queue.Enqueue(value);

        foreach (var v in values)

            queue.Enqueue(v);

        return new Service<Queue<T>>(queue);

    }

}

This class I created as my implementation of service locator earlier when I tried use this pattern to decouple the code. As you can see from code, service implementations can be nested and are specific for current thread (I failed to create services that work between multiple thread because of strange CallContext class behaviour when someone called EndInvoke()). If anyone is interested here is example of usage:

using (new Service<IQuestion>(new MockAskEmailWindow()))

{

    //some code

    using (new Service<IQuestion>(new MockAskEmailWindow()))

    {

        //some other code that uses other version of IQuestion

        //by calling Service<IQuestion>.Value

    }

    //here we use again the first IQuestion

}

But back to generic method testing as you can see from example above the scheme is following:

  • Create mocks using Rhino Mocks
  • put them in the service queue
  • run tested method
  • When mocked type instance is created it takes mock from service queue
  • All calls to newly created object are redirected to mock created in test

And that’s it.

Just one more note: Because of EnvDTE unavailability to read generic constraints (and because it was failing when I called CodeMethod.StartPoint and CodeMethod.EndPoint) I was unable to mock such methods. I solved this by don’t mocking it at all. You must implement such method in your part of partial mock class.

Hope this helps someone

Brak komentarzy: