Embedding an ASP .NET application in a unit test (part 2)

In the previous part of this series I talked about how we set up a unit test to run a web application under an embedded instance of the Cassini web server. This takes care of the central component of the diagram below that illustrates our architecture.

So what about mocking the entire backend?

Step 2: mocking an object and redirecting it back to the test assembly

Recall what little access we have to the web server:

server.StartServer("C:\SymbolSource\SymbolSource.Gateway.NuGet");
server.NormalizeUrl("/");

Apart from that, we can only influence the application by using it’s HTTP interface or by modifying web.config. Obviously we don’t want to achieve test compatibility by extending the public access surface.

Before I talk about our solution, let’s sum up the requirements:

  • we want to inject a mock of a SOAP API into an isolated web application,
  • building the mock should be as simple as possible, since we may want build a different one in each unit test,
  • we need complete access to the mock object to verify if and how it was called,
  • the object should be statefull, otherwise we will need to manage sessions ourselves,
  • the final solution should work on Mono.

The only possible solution meeting these requirements that we could think of was .NET Remoting. What we did was expose an API implementation with .NET Remoting, and modify web.config before test execution to point to the location of this object. The fact that the gateway never uses the WebService class directly, only a wrapper exposed through IoC, greatly helped.

Changes to application under test

Almost everything user-accessible in SymbolSource is coded against the following interface (of course it’s a bit bigger in reality), which wraps the server SOAP API:

public interface IManagementProxy
{
    Project[] GetProjects(ref Repository repository);
    void CreateOrUpdateProject(Project project);
    void CreateProject(Project project);
    void UpdateProject(Project project);
    void DeleteProject(Project project);
}

Our container of choice is Castle.Windsor. Here is the modified component registration code that takes care of redirecting IManagementProxy thorugh .NET Remoting to a mock object:

var redirect = ConfigurationManager.AppSettings["ManagementRedirect"];

if (string.IsNullOrEmpty(redirect))
{
    kernel.Register(Component.For<IManagementProxy>()
        .ImplementedBy());
}
else
{
    kernel.Register(Component.For<IManagementProxy>()
        .UsingFactoryMethod(() => ManagementProxyFactory.Activate(redirect)));
}

As you can see, if no redirect was requested in web.config, IManagementProxy will be implemented by a really boring class that just creates the WebService class generated by Visual Studio’s Add service reference dialog. If there is, however, a factory method is registered that looks like this:

public static class ManagementProxyFactory
{
    public static IManagementProxy Activate(string uri)
    {
        var remoteProxyFactory = (IManagementProxyFactory)Activator.GetObject(
            typeof(IManagementProxyFactory), uri);

        return remoteProxyFactory.Create();
    }
}

public interface IManagementProxyFactory
{
    IManagementProxy Create();
}

Nothing really fancy, just using the URI passed in from web.config to activate a remote object, which in this case is factory returning objects implementing IManagementProxy. We need a factory here, because there is no way to control .NET Remoting object instantiation on the server (unit test). It’s always constructed by the .NET Framework, either in singleton or per-request mode, and we want to provide our own pre-configured instance. IManagementProxy and IManagementProxyFactory are classes that of course need to come from a library shared between the web application and test project.

.NET Remoting setup in a unit test

Let’s switch now to an actual test case. Here’s an example that tries to push a package:

public class NuGetPublishTests : GatewayTestsBase
{
    [Fact]
    public void TestPublish()
    {
        var mock = ...

        ManagementProxyFactory.Register(mock);

        try
        {
            nuget.Push(
                "Test-1.0.0.0.nupkg",
                "A910EAD8-D0D9-4D39-941B-11D194508F96",
                NormalizeUrl("/Public/NuGet"));
        }
        finally
        {
            ManagementProxyFactory.Unregister(mock);
        }
    }
}

I’ll get back to how a mock is created in just a moment. Let’s have a look at the Register/Unregister methods, which are reponsible for registering our mock with .NET Remoting.

public class ManagementProxyFactory : MarshalByRefObject, IManagementProxyFactory
{
    private static readonly Stack uris;
    private static readonly IDictionary uses;
    private static readonly Stack proxies;

    static ManagementProxyFactory()
    {
        uris = new Stack();
        uses = new Dictionary();
        proxies = new Stack();
    }

    public static void Register(IManagementProxy proxy, string uri)
    {
        var parsedUri = new Uri(uri);

        if (parsedUri.Scheme != "tcp" || parsedUri.Host != "localhost" || parsedUri.Segments.Length != 2)
            throw new ArgumentException("uri");

        RemotingConfiguration.RegisterWellKnownServiceType(
            typeof(ManagementProxyFactory), parsedUri.Segments[1], WellKnownObjectMode.Singleton);

        if (!uses.ContainsKey(parsedUri.Port))
        {
            uses[parsedUri.Port] = 0;
            ChannelServices.RegisterChannel(new TcpServerChannel(parsedUri.Port.ToString(), parsedUri.Port));
        }

        uris.Push(parsedUri);
        uses[parsedUri.Port]++;
        proxies.Push(proxy);
    }

    public static void Unregister(IManagementProxy proxy, string uri)
    {
        var parsedUri = new Uri(uri);

        if (proxies.Count < 1 || uris.Count < 1 || proxy != proxies.Peek() || parsedUri != uris.Peek())
            throw new InvalidOperationException();

        uris.Pop();
        uses[parsedUri.Port]--;
        proxies.Pop();

        if (uses[parsedUri.Port] == 0)
        {
            ChannelServices.UnregisterChannel(ChannelServices.GetChannel(parsedUri.Port.ToString()));
            uses.Remove(parsedUri.Port);
        }
    }

    public IManagementProxy Create()
    {
        return proxies.Peek();
    }
}

That’s a bit chunk of code, I admit, but we wanted it to be failry robust and flexible, so it does endpoint reference counting, and maintains a stack of mock proxy objects to allow scoped switching. The most important part is calling RemotingConfiguration.RegisterWellKnownServiceType to register the factory, ChannelServices.RegisterChannel to open a TCP listener and IManagementProxyFactory.Create, which returns the most recently registered mock.

Here’s the part of web.config defining the redirect URI. A very good idea would be to apply it as a transformation on a copy of the web application before testing it. Or you can just keep the test-friendly version in your source code repository and remove the redirect as part of a normal deployment.

<configuration>
    <appSettings>
        <add key="ManagementRedirect" value="tcp://localhost:64530/ManagementProxyFactory" />
    </appSettings>
</configuration>

So what about creating the mock? Can I use a mocking library? Or do I need to implement a stub?

In our solution we went for custom classes implementing IManagementProxy, because we needed to mock quite complex behaviors. But it’s also perfectly achievable with e.q. Moq, like this:

var mockBase = new Mock(MockBehavior.Strict);
mockBase.CallBase = true;

var mock = mockBase.As<IManagementProxy>();
mock.Setup(proxy => proxy.CreateProject(It.IsAny<Project>()))
    .Throws(new Exception("Already exists");

ManagementProxyFactory.Register(mock.Object);

And that’s it! We now have a unit test that performs the following awesomeness:

  • prepares an object mocking an API, with a behavior tailored for the current test,
  • registers the mock with .NET Remoting to make it accessible across app domain boundaries,
  • initializes an embedded web server to run the application under test,
  • uses the application’s HTTP interface to perform some actions and validate their results.

Step 3: automate external console applications to simulate real-life conditions

As I’ve mentioned before, we do not try to mimic the behavior of existing applications. Instead we run nuget.exe and o.exe in our tests with a redirected console to catch their outputs.

Some thoughts on this matter will be part 3 of the series. So again, stay tuned!

Advertisement
Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: