2

I'm trying to write some unit tests for my ASP.NET MVC code, but I'm hitting a road block.

I have entity partial classes (in a business class library assembly) that need to determine a URL to call for an action method and controller. In order to do that, I found this snippet of code which works nicely - but alas, this uses the HttpContext.Current and thus prevents me from writing any unit tests:

public string NavigateUrl 
{
    get 
    {
            HttpContextWrapper httpContextWrapper = new HttpContextWrapper(HttpContext.Current);
            UrlHelper urlHelper = new UrlHelper(new RequestContext(httpContextWrapper, RouteTable.Routes.GetRouteData(httpContextWrapper)));

            string url = urlHelper.Action("SomeAction", "MyController");
    }
}

I am reading about the HttpContextBase - but how does this come into play here?? Or is there another way to determine an action URL inside an entity class (that is in a business assembly - NOT the MVC project and not inside a controller or other MVC infrastructure class)?

Update: I need to return this URL from an entity class as a string, since I need to use it in a grid as the navigation URL of a hyperlink. And in reality, there are numerous conditions being checked and the URL string returned can be one of several possibilities - so I cannot just replace it by a single controller call...

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • 3
    Possible duplicate of [MVC, not "supposed" to use HttpContext.Current anymore?](https://stackoverflow.com/questions/9119345/mvc-not-supposed-to-use-httpcontext-current-anymore) – Koby Douek Nov 16 '17 at 11:54
  • Also, look at this: https://stackoverflow.com/questions/32660347/alternative-to-using-request-url-and-request-urlreferrer-to-get-the-current-link - It has a helper class which does exactly what you are looking for. – Koby Douek Nov 16 '17 at 11:56
  • Do you actually need the `HttpContext` ? because you can directly invoke the actions using `new Controller().SomeAction(params)` – Orel Eraki Nov 16 '17 at 11:56
  • @OrelEraki: I want to return a NavigateUrl string so that I can use it in a grid as the target of a hyperlink column – marc_s Nov 16 '17 at 11:58
  • 1
    @marc_s as these are meant to be entities I am assuming explicit dependencies via constructor injection is not really an option. Though considered an anti-pattern you could consider a static service locator that would allow you to change the abstraction as needed for testing. – Nkosi Nov 16 '17 at 12:00
  • @marc_s Encapsulate that functionality in a service that can be abstracted and have a static factory method that will provide you with the service. – Nkosi Nov 16 '17 at 12:04
  • @marc_s let me know your thoughts on my suggestions and I'll if I can provide a more detailed explanation in an answer. – Nkosi Nov 16 '17 at 12:07
  • In addition to `HttpContext`, keep in mind the route table is dependent on where your class library is running, e.g. if you use this library in a non-web context, the route table will be empty unless you are populating it outside of your web application. – Tim M. Nov 16 '17 at 12:13
  • 1
    You say that this is located in business assembly without any relation to MVC, and yet you are using a bunch of MVC classes (HttpContext, RouteTable, and so on) - how so? – Evk Nov 16 '17 at 12:47
  • It seems it is by design. See the relevant discussion about composing links outside the web app context https://stackoverflow.com/questions/46096068/asp-net-core-2-0-creating-urlhelper-without-request – Dmitry Pavlov Nov 16 '17 at 15:01
  • @DmitryPavlov: thanks - but that's all for ASP.NET Core - I'm still on ASP.NET MVC 5 .... – marc_s Nov 16 '17 at 18:20
  • @marc_s any feedback re provided answer? – Nkosi Nov 20 '17 at 12:06
  • @Nkosi: haven't had the time to really investigate this yet - patience, my friend ... – marc_s Nov 20 '17 at 12:25
  • 1
    @marc_s not a problem. was just following up. take your time. happy coding. – Nkosi Nov 20 '17 at 12:48

1 Answers1

1

Create an abstraction to represent the desired functionality.

For example

public interface IUrlHelper {
    string Action(string actionName, string controllerName);
    //TODO: create other desired members to be exposed
}

You then create a factory for that abstraction. Since you are not injecting it into the entities we are using a service locator ani-pattern.

public static class UrlHelperFactory {

    public static Func<IUrlHelper> Create = () => {
        throw new NotImplementedException();
    };

}

The helper and factory are not coupled to anything and could live anywhere in the solution.

The following test mocks the service to allow the entity to be tested in isolation.

[TestClass]
public class UrlHelperFactory_Should {

    public class MyTestEntity {
        public string NavigateUrl {
            get {
                var urlHelper = UrlHelperFactory.Create();
                string url = urlHelper.Action("SomeAction", "MyController");
                return url;
            }
        }
    }

    [TestMethod]
    public void Generate_NavigationUrl() {
        //Arrange
        var mockHelper = Mock.Of<IUrlHelper>();
        UrlHelperFactory.Create = () => {
            return mockHelper;
        };
        var expected = "http://my_fake_url";
        Mock.Get(mockHelper)
            .Setup(_ => _.Action(It.IsAny<string>(), It.IsAny<string>()))
            .Returns(expected);

        var sut = new MyTestEntity();

        //Act
        var actual = sut.NavigateUrl;

        //Assert
        actual.Should().NotBeNullOrWhiteSpace()
            .And.Be(expected);
    }
}

In production code at the composition root you make sure that the factory knows how to build the service

UrlHelperFactory.Create = () => {
    var httpContextWrapper = new HttpContextWrapper(HttpContext.Current);
    var urlHelper = new UrlHelper(new RequestContext(httpContextWrapper, RouteTable.Routes.GetRouteData(httpContextWrapper)));
    return new DefaultUrlHelperWrapper(urlHelper);
};

Where a wrapper could look like this...

internal class DefaultUrlHelperWrapper : IUrlHelper {
    private UrlHelper urlHelper;

    public DefaultUrlHelperWrapper(UrlHelper urlHelper) {
        this.urlHelper = urlHelper;
    }

    public string Action(string actionName, string controllerName) {
        return urlHelper.Action(actionName, controllerName);
    }

    //TODO: Implement other members
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472