Sunday, November 1, 2009

Configuring fake objects on a global scale

In this post I’ll describe a way that lets you specify default configurations for specific types so that any time a fake of that type is created this default configuration will be applied.

In FakeItEasy there’s a concept called a fake object container represented by the interface IFakeObjectContainer which has two methods, one for resolving/creating fakes and one for configuring fakes, in this post I’m going to focus on the configuration part.

namespace FakeItEasy.Api
{
    using System;
    using System.Collections.Generic;

    /// <summary>
    /// A container that can create fake objects.
    /// </summary>
    public interface IFakeObjectContainer
    {
        /// <summary>
        /// Creates a fake object of the specified type using the specified arguments if it's
        /// supported by the container, returns a value indicating if it's supported or not.
        /// </summary>
        /// <param name="typeOfFakeObject">The type of fake object to create.</param>
        /// <param name="arguments">Arguments for the fake object.</param>
        /// <param name="fakeObject">The fake object that was created if the method returns true.</param>
        /// <returns>True if a fake object can be created.</returns>
        bool TryCreateFakeObject(Type typeOfFakeObject, IEnumerable<object> arguments, out object fakeObject);

        /// <summary>
        /// Applies base configuration to a fake object.
        /// </summary>
        /// <param name="typeOfFakeObject">The type the fake object represents.</param>
        /// <param name="fakeObject">The fake object to configure.</param>
        void ConfigureFake(Type typeOfFakeObject, object fakeObject);
    }
}

Using the scoping feature you can actually plug in your own container for a given scope like this:

using (Fake.CreateScope(new MyOwnContainer()))
{ 
    
}

This means that within this scope your container will be used. Let’s focus on the ConfigureFake-method. This method will be called ANY time a fake object is created by FakeItEasy and lets us hook into the creation process and apply configuration to the generated fake object. This provides an easy way of defining default configuration for fakes of certain types which allows you to in your tests focus on configuring the values that are significant to the test.

At the moment the default container is a class called NullFakeObjectContainer, which I’m sure you can guess does exactly nothing. However, there is an assembly distributed in the latest versions named FakeItEasy.Mef.dll. If you in your test-project adds a reference to this assembly FakeItEasy will automatically pick this up and use the MefContainer as the default container. And this is where the fun begins.

The MEF assembly exposes two types that are significant to this feature, the most important one being IFakeConfigurator and the most used one being FakeConfigurator<T>.

namespace FakeItEasy.Mef
{
    using System;
    using System.ComponentModel.Composition;

    /// <summary>
    /// Provides configurations for fake objects of a specific type.
    /// </summary>
    [InheritedExport(typeof(IFakeConfigurator))]
    public interface IFakeConfigurator
    {
        /// <summary>
        /// The type the instance provides configuration for.
        /// </summary>
        Type ForType { get; }

        /// <summary>
        /// Applies the configuration for the specified fake object.
        /// </summary>
        /// <param name="fakeObject">The fake object to configure.</param>
        void ConfigureFake(object fakeObject);
    }
}

As you see this interface is tagged with the InheritedExportAttribute from MEF, which means that when you implement this interface in a type that type is automatically exported.

As I said the most used one is probably FakeConfigurator<T> which is a base implementation of this interface that lets you implement it very easily. Let’s say for example that you want to provide a common configuration for the HttpRequestBase type, all you do is create a class that inherits FakeConfigurator<HttpRequestBase> and by magic this is exported by MEF and included in the default container.

public class RequestConfiguration
    : FakeConfigurator<HttpRequestBase>
{
    public override void ConfigureFake(HttpRequestBase fakeObject)
    {
        Configure.Fake(fakeObject)
            .CallsTo(x => x.Form)
            .Returns(new NameValueCollection());

        Configure.Fake(fakeObject)
            .CallsTo(x => x.UserAgent)
            .Returns("Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13");

        Configure.Fake(fakeObject)
            .CallsTo(x => x.ContentEncoding)
            .Returns(Encoding.UTF8);

        Configure.Fake(fakeObject)
            .CallsTo(x => x.Url)
            .Returns(new Uri("http://foo.com/"));

        Configure.Fake(fakeObject)
            .CallsTo(x => x.RawUrl)
            .Returns(x => ((HttpRequestBase)x.FakedObject).Url.AbsoluteUri);

        Configure.Fake(fakeObject)
            .CallsTo(x => x.UrlReferrer)
            .Returns(new Uri("http://bar.com/"));
    }
}

Now any time you create a faked HttpRequestBase it will automatically be configured with this default configuration and you will only have to override this default configuration with specific values you need for your tests.

[TestFixture]
public class Tests
{

    [Test]
    public void Test_something_that_is_dependent_on_the_UserAgent()
    {
        var request = A.Fake<HttpRequestBase>();

        Console.Write(request.UserAgent); // Will print "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13"

        Configure.Fake(request)
            .CallsTo(x => x.UserAgent)
            .Returns("some other agent");

        Console.Write(request.UserAgent); // Will print "some other agent"
    }
}

No comments:

Post a Comment