11

I have the following controller action

public void Post(Dto model)
{
    using (var message = new MailMessage())
    {
        var link = Url.Link("ConfirmAccount", new { model.Id });

        message.To.Add(model.ToAddress);
        message.IsBodyHtml = true;
        message.Body = string.Format(@"<p>Click <a href=""{0}"">here</a> to complete your registration.<p><p>You may also copy and paste this link into your browser.</p><p>{0}</p>", link);

        MailClient.Send(message);
    }
}

To test this I need to setup the controller context

var httpConfiguration = new HttpConfiguration(new HttpRouteCollection { { "ConfirmAccount", new HttpRoute() } });
var httpRouteData = new HttpRouteData(httpConfiguration.Routes.First());
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "http://localhost");
sut = new TheController
{
    ControllerContext = new HttpControllerContext(httpConfiguration, httpRouteData, httpRequestMessage),
    MailClient = new SmtpClient { PickupDirectoryLocation = location }
};

This seems like a lot of setup to test the creation of a link. Is there a cleaner way to do this? I have read about in-memory servers but that looks like it applies more to the httpclient than testing the controller directly.

krillgar
  • 12,596
  • 6
  • 50
  • 86
Jason Meckley
  • 7,589
  • 1
  • 24
  • 45
  • +1 I thought the whole point of REST services was to allow for linkable resources. I am really unhappy with the WebAPI Link/Url utilities. Referencing the route name seems so fragile and the testing story is equally as painful. Hopefully there are some improvements coming... – Vinney Kelly Apr 10 '13 at 16:40

3 Answers3

16

I started using this approach with Web API 2.0.

If you're using a mocking library (and you really should for any real world unit tests), you are able to directly mock the UrlHelper object as all of the methods on it are virtual.

var mock = new Mock<UrlHelper>();
mock.Setup(m => m.Link(It.IsAny<string>(), It.IsAny<object>())).Returns("test url");

var controller = new FooController {
    Url = mock.Object
};

This is a far cleaner solution than Ben Foster's answer, as with that approach, you need to add routes to the config for every name that you're using. That could easily change or be a ridiculously large number of routes to set up.

krillgar
  • 12,596
  • 6
  • 50
  • 86
  • Should be the accepted answer. Simple and elegant. I needed this code too. var context = new HttpRequestContext { Url = mockUrlHelper.Object }; request.Properties.Add(HttpPropertyKeys.RequestContextKey, context); – CountZero Dec 03 '16 at 23:04
  • @CountZero What caused you to need that? If you want to add that as your own answer, and build off of mine, that could be good, or we can combine it here. Up to you, but some context (no pun intended) would be helpful. – krillgar Dec 04 '16 at 20:52
12

Below is the absolute minimum code required to test UrlHelper without any kind of mocking library. The thing that threw me (and took me some time to track down) was that you need to set the IHttpRouteData of the request. If you don't the IHttpRoute instance will fail to generate a virtual path resulting in an empty URL.

    public class FooController : ApiController
    {
        public string Get()
        {
            return Url.Link(RouteNames.DefaultRoute, new { controller = "foo", id = "10" });
        }
    }

    [TestFixture]
    public class FooControllerTests
    {
        FooController controller;

        [SetUp]
        public void SetUp()
        {
            var config = new HttpConfiguration();

            config.Routes.MapHttpRoute(
                name: "Default",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional });

            var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost");
            request.Properties[HttpPropertyKeys.HttpConfigurationKey] = config;
            request.Properties[HttpPropertyKeys.HttpRouteDataKey] = new HttpRouteData(new HttpRoute());

            controller = new FooController
            {
                Request = request
            };
        }

        [Test]
        public void Get_returns_link()
        {
            Assert.That(controller.Get(), Is.EqualTo("http://localhost/api/foo/10"));
        }
    }
Ben Foster
  • 34,340
  • 40
  • 176
  • 285
  • I had to also create the `ControllerContext` property of `FooController`, including its `ControllerDescriptor`. I inherited this code and am not sure which ver of web api I'm using, but it's MVC 4. – Shawn South Aug 28 '14 at 17:46
  • @GetFuzzy A little late, but I just got this figured out and am adding an answer. – krillgar Sep 16 '15 at 14:03
  • I don't like the fact that you have to re-define your routes in your tests. – Jim Aho Nov 23 '15 at 15:00
2

I'm running into the same idiocy. All the references I can find want you to Mock the Request/Controller, which is (as you pointed out) a lot of work.

Specific references:

I haven't gotten around to trying the actual Mocking frameworks, so I have a helper class to "build" my controller. So instead of

sut = new TheController { ... }

I use something like:

// actually rolled together to `sut = MyTestSetup.GetController(method, url)`
sut = new TheController()...
MyTestSetup.FakeRequest(sut, HttpMethod.Whatever, "~/the/expected/url");

For reference, the method is basically:

public void FakeRequest(ApiController controller, HttpMethod method = null, string requestUrl = null, string controllerName = null) {
    HttpConfiguration config = new HttpConfiguration();
    // rebuild the expected request
    var request = new HttpRequestMessage( null == method ? this.requestMethod : method, string.IsNullOrWhiteSpace(requestUrl) ? this.requestUrl : requestUrl);
    //var route = System.Web.Routing.RouteTable.Routes["DefaultApi"];
    var route  = config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}");

    // TODO: get from application?  maybe like https://stackoverflow.com/a/5943810/1037948
    var routeData = new HttpRouteData(route, new HttpRouteValueDictionary { { "controller", string.IsNullOrWhiteSpace(controllerName) ? this.requestController : controllerName } });

    controller.ControllerContext = new HttpControllerContext(config, routeData, request);

    // attach fake request
    controller.Request = request;
    controller.Request.Properties[/* "MS_HttpConfiguration" */ HttpPropertyKeys.HttpConfigurationKey] = config;
}
Community
  • 1
  • 1
drzaus
  • 24,171
  • 16
  • 142
  • 201
  • I'm now using [RazorGenerator](http://razorgenerator.codeplex.com/) to build the text instead of manually building the links in the controller. – Jason Meckley Aug 21 '12 at 00:39