Mock ServerVariables in the HttpContext.Current.Request

6.7k views Asked by At

One of my services uses server variable provided by IIS through such code

var value = System.Web.HttpContext.Current.Request.ServerVariables["MY_CUSTOM_VAR"];

What I've tried is to mock those object and insert my own variable/collection and check few cases(e.g. variable is missing, value is null ...) I'm able to create instances of HttpContext, HttpRequest, HttpResponse and assign them properly however each of them is just a plain class without interface or virtual properties and initialization of ServerVariables happens somewhere under the hood.

HttpContext mocking:

var httpRequest = new HttpRequest("", "http://excaple.com/", "");
var stringWriter = new StringWriter();
var httpResponse = new HttpResponse(stringWriter);
var httpContextMock = new HttpContext(httpRequest, httpResponse);
HttpContext.Current = httpContextMock;

Attempt #1 Call private method via reflection

var serverVariables = HttpContext.Current.Request.ServerVariables;
var serverVariablesType = serverVariables.GetType();
MethodInfo addStaticMethod = serverVariablesType.GetMethod("AddStatic", BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic);

addStaticMethod.Invoke(serverVariables, new object[] {"MY_CUSTOM_VAR", "value"});

Failed with error saying that collection is readonly.

Attempt #2 Replace ServerVariables with my own instance

var request = HttpContext.Current.Request;
var requestType = request.GetType();
var variables = requestType.GetField("_serverVariables", BindingFlags.Instance | BindingFlags.NonPublic);

variables.SetValue(request, new NameValueCollection
{
    { "MY_CUSTOM_VAR", "value" }
});

Failed with error that it's not possible to cast NameValueCollection to HttpServerVarsCollection. That's because HttpServerVarsCollection is actually a internal class so I couldn't either to make an instance of it or cast to it.

So the question is - How can I mock ServerVariables or insert value there? Thanks

3

There are 3 answers

0
galenus On

This is exactly the case where you should refactor your code.

Example of refactored code:

public class MyService
{
    public ServiceResult SomeMethod()
    {
        ...
        var value = GetVariable(System.Web.HttpContext.Current, "some var name");
        ...
    }

    protected virtual string GetVariable(HttpContext fromContext, string name)
    {
        return fromContext.Request.ServerVariables[name];
    }

}

In your test:

class MyTestableService : MyService
{
    protected override string GetVariable(HttpContext fromContext, string name)
    {
        return MockedVariables[name];
    }

    public Dictionary<string, string> MockedVariables { get; set; }
}

[Test]
public void TestSomeMethod()
{
    var serviceUnderTest = 
           new MyTestableService 
           { 
                MockedVariables = new Dictionary<string, string>
                {
                     { "some var name", "var value" }
                }
           };

     //more arrangements here

     Assert.AreEqual(expectedResult, serviceUnderTest.SomeMethod());
 }

Of course, you can extract the method to a separate dependency altogether.

0
Birey On

I found the answer here http://forums.asp.net/t/1125149.aspx

It is not possible to access the objects that stores server variables. But it can be accessed using reflection. So using reflection we can set the values that we want. Following extension method will help set any server variable.

public static void AddServerVariable(this HttpRequest request, string key, string value)
    {
        BindingFlags temp = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
        MethodInfo addStatic = null;
        MethodInfo makeReadOnly = null;
        MethodInfo makeReadWrite = null;

        Type type = request.ServerVariables.GetType();
        MethodInfo[] methods = type.GetMethods(temp);
        foreach (MethodInfo method in methods)
        {
            switch (method.Name)
            {
                case "MakeReadWrite":
                    makeReadWrite = method;
                    break;
                case "MakeReadOnly":
                    makeReadOnly = method;
                    break;
                case "AddStatic":
                    addStatic = method;
                    break;
            }
        }
        makeReadWrite.Invoke(request.ServerVariables, null);
        string[] values = { key, value };
        addStatic.Invoke(request.ServerVariables, values);
        makeReadOnly.Invoke(request.ServerVariables, null);
    }
1
Vee Trairattanapa On

The problem is, HttpContext and HttpRequest objects are not mockable.

HttpContext

This is the vintage asp.net context. The problem with this is that it has no base class and isn't virtual, and hence is unusable for testing (cannot mock it). It's recommended to not pass it around as function arguments, instead pass around variables of type HttpContextBase.

HttpContextBase

This is the (new to c# 3.5) replacement to HttpContext. Since it is abstract, it is now mockable. The idea is that your functions that expect to be passed a context should expect to receive one of these. It is concretely implemented by HttpContextWrapper

HttpContextWrapper

Also new in C# 3.5 - this is the concrete implementation of HttpContextBase. To create one of these in a normal webpage, use new HttpContextWrapper(HttpContext.Current).

The idea is that to make your code unit-testable, you declare all your variables and function parameters to be of type HttpContextBase, and use an IOC framework eg Castle Windsor to get it injected. In normal code, castle is to inject the equivalent of 'new HttpContextWrapper(HttpContext.Current)', whereas in test code you're to be given a mock of HttpContextBase.

(source: http://www.splinter.com.au/httpcontext-vs-httpcontextbase-vs-httpcontext/)

You need to wrap it with HttpContextBase like so:

var result = YourMethod(new HttpContextWrapper(HttpContext.Current));

Then when you write your test for YourMethod(HttpContextBase context), you can easily mock the HttpContextBase using Moq:

var request = new Mock<HttpRequestBase>(MockBehavior.Strict);
request.Setup(x => x.ServerVariables).Returns(new System.Collections.Specialized.NameValueCollection{
  { "MY_CUSTOM_VAR", "your value here" }
});

var context = new Mock<HttpContextBase>();
context.SetupGet(x => x.Request).Returns(request.Object);

Then your HttpContext object can be called with context.Object;