Friday, 10 November 2017

Unit testing HttpRequestArgs with Sitecore 9

Working on a project currently with Sitecore CMS, which is currently undergoing an upgrade to version 9.

We had this piece of code, used in the context of unit testing classes that inherit from HttpRequestProcessor, such as custom page resolvers. To use these classes you override a method Process() that takes an instance of HttpRequestArgs.

Constructing these classes from a unit test isn't straightforward and up to now had relied on a method detailed by James-West Sadler on his blog here.

    public static HttpRequestArgs CreateHttpRequestArgs(string url)
    {
        var httpRequest = new HttpRequest(string.Empty, url, string.Empty);
        var httpResponse = new HttpResponse(new StringWriter());
        var httpContext = new HttpContext(httpRequest, httpResponse);

        var requestArgs = new HttpRequestArgs(new HttpContextWrapper(httpContext), HttpRequestType.End);
        
        // Reflection used to call the private Initialize method on the HttpRequestArgs object, which amongst other things 
        // populates the URL on the object itself.
        var method = requestArgs.GetType().GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Instance);
        method?.Invoke(requestArgs, null);

        return requestArgs;
    }

Following the upgrade to Sitecore 9 though this began to fail with a null reference exception in the invoke of the Initialize method using reflection.

Obviously using reflection like this is a little risky, as we have no means of being sure something is going to be backwardly compatible here following an upgrade. There's a feedback item to make these more easily testable here.

To resolve I first used dotPeek to de-compile the assembly and code see the following changes between this method in version 8:

    protected internal override void Initialize()
    {
        this._url = RequestUrl.Parse(this._context.Request);
        this._url.QueryString = this._url.QueryString;
    }

As compared to version 9:

    protected internal override void Initialize()
    {
        this.Url = Sitecore.Web.RequestUrl.Parse(this.HttpContext.ApplicationInstance.Context.Request);
        this.Url.QueryString = this.Url.QueryString;
    }

So my first attempt was to try to pass in the ApplicationInstance object which was causing the null reference exception, like this:

    public static HttpRequestArgs CreateHttpRequestArgs(string url)
    {
        var httpRequest = new HttpRequest(string.Empty, url, string.Empty);
        var httpResponse = new HttpResponse(new StringWriter());
        var httpContext = new HttpContext(httpRequest, httpResponse)
            {
                ApplicationInstance = new HttpApplication()
            };

        var requestArgs = new HttpRequestArgs(new HttpContextWrapper(httpContext), HttpRequestType.End);
        
        // Reflection used to call the private Initialize method on the HttpRequestArgs object, which amongst other things 
        // populates the URL on the object itself.
        var method = requestArgs.GetType().GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Instance);
        method?.Invoke(requestArgs, null);

        return requestArgs;
    }

This didn't work though, as still the Context property was null.

Next was an attempt to mock this property, as follows:

    public static HttpRequestArgs CreateHttpRequestArgs(string url)
    {
        var httpRequest = new HttpRequest(string.Empty, url, string.Empty);
        var httpResponse = new HttpResponse(new StringWriter());
        var httpContext = new HttpContext(httpRequest, httpResponse);

        var mockApplication = new Mock<HttpApplication>();
        mockApplication.SetupGet(x => x.Context).Returns(httpContext);
        httpContext.ApplicationInstance = mockApplication.Object;

        var requestArgs = new HttpRequestArgs(new HttpContextWrapper(httpContext), HttpRequestType.End);
        
        // Reflection used to call the private Initialize method on the HttpRequestArgs object, which amongst other things 
        // populates the URL on the object itself.
        var method = requestArgs.GetType().GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Instance);
        method?.Invoke(requestArgs, null);

        return requestArgs;
    }

Unfortunately though Context is not a virtual property, and as such can't be mocked by tools like Moq.

Final, and successful, attempt was to revert once again to reflection - possibly pushing another problem down the line, but looking a reasonable solution for now.

    public static HttpRequestArgs CreateHttpRequestArgs(string url)
    {
        var httpRequest = new HttpRequest(string.Empty, url, string.Empty);
        var httpResponse = new HttpResponse(new StringWriter());
        var httpContext = new HttpContext(httpRequest, httpResponse);

        var requestArgs = new HttpRequestArgs(new HttpContextWrapper(httpContext), HttpRequestType.End);
        
        // Reflection used to set the protected Url property
        var property = requestArgs.GetType().GetProperty("Url", BindingFlags.Public | BindingFlags.Instance);
        property?.SetValue(requestArgs, new RequestUrl(httpRequest));

        return requestArgs;
    }


No comments:

Post a Comment