Testing views with RazorGenerator

Razor what?

RazorGenerator is a hidden gem that lets you generate and pre-compile what would otherwise be generated and compiled at runtime. Your Razor. Not only does it give you a startup time boost, but it lets you unit test your views. The latter is the focus of this post.

We'll continue to build on the project I described in my post on generating documentation with NUnit. It's a simple use case where an imaginary CMS has a feature to display events from a third party site. Here are the tests I've got so far:

NUnit output

In my previous post, I left the rendering test inconclusive. I like keeping inconclusive tests around as reminders of stuff I've got left to do. Let's have a quick look at the passing code before we dive into the rendering bits.

Basic HTTP integration and conversion tests

The first large piece of what's in there for now is a way to remove the physical dependency on the third party site. I like to stub away IO as far out as I can so I can test as much of my code quickly, yet as integrated as possible. In other words, as many participating classes as possible. Since we're making an example here, I'm just using the HttpClient directly from the controller. The HttpClient is hard to mock or fake, but it has an inner dependency that we can pass as an argument to a constructor: HttpMessageHandler. It has the innermost function that the HttpClient uses for any operation. It also has the rare trait of being protected virtual, so we can stub it out. For this example, I'm just using a fake one that records requests and returns responses for registered URLs. Here it is:

public class FakeMessageHandler : HttpMessageHandler
{
    private readonly Dictionary<string, string> responses = new Dictionary<string,string>();
    public List<string> Calls { get; } = new List<string>();

    public void Register(string url, string response)
    {
        responses.Add(url, response);
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var url = request.RequestUri.ToString();

        Calls.Add(url);

        if (!responses.ContainsKey(url))
        { 
            return Task.FromResult(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.NotFound,
                Content = new StringContent("{}")
            });
        }

        return Task.FromResult(new HttpResponseMessage
        {
            Content = new StringContent(responses[url])
        });
    }
}

We'll not dwell further on this one in this post, but you'll need it to run the following examples if you want to tag along.

The tests that verifies that we call the right URL on the third party site is pretty simple. It checks the Calls collection on the FakeMessageHandler. Here's the test and the setup code needed:

[TestFixture]
public class When_Displaying_An_Event
{
    [Test]
    [DocumentationOrder(0)]
    [Description(@"
    Events are provided at eventsite with a REST api at the URL:
    https://eventsite/api/{id}
    ")]
    public void It_Is_Fetched_By_Id_From_The_Event_Server()
    {
        eventController.Event(234).Wait();

        Assert.AreEqual(
            "https://eventsite/api/234", 
            httpMessageHandler.Calls.Single()
        );
    }

    // ... omitted other tests

    [SetUp]
    public void Setup()
    {
        httpMessageHandler = new FakeMessageHandler();
        httpClient = new HttpClient(httpMessageHandler);
        eventController = new EventController(httpClient);
    }

    private FakeMessageHandler httpMessageHandler;
    private HttpClient httpClient;
    private EventController eventController;
}

And the controller:

public class EventController : Controller
{
    private readonly HttpClient httpClient;

    public EventController(HttpClient httpClient)
    {
        this.httpClient = httpClient;
    }

    public async Task<PartialViewResult> Event(int id)
    {
        var requestUri = "https://eventsite/api/" + id;
        var result = await httpClient.GetAsync(requestUri);
        var response = await result.Content.ReadAsStringAsync();
        // ... omitted further processing for now
    }
}

We pass the FakeMessageHandler instance to a new HttpClient effectively making it a stub. We can control the response for individual URLs, and verify that the right ones were called.

Next step is to convert it to a valid model we can use in our view. We've got a second test asserting that:

[Test]
[DocumentationOrder(1)]
[Description("The JSON response from the event server is deserialized as the Event type.")]
public void It_Is_Converted_To_An_Event_Model()
{
    var result = GetConcertEvent();
    var model = (Event)result.Model;

    Assert.AreEqual("Concert of your life", model.Name);
    Assert.AreEqual(new DateTime(2049,12,31,23,59,59), model.Time);
    Assert.AreEqual("https://eventsite/123", model.Url);
}

private PartialViewResult GetConcertEvent()
{
    httpMessageHandler.Register(
        "https://eventsite/api/123",
        "{\"name\":\"Concert of your life\", \"time\":2524607999}"
    );

    var result = eventController.Event(123).Result;
    return result;
}

And here's the rest of the controller code creating that model:

public async Task<PartialViewResult> Event(int id)
{
    var requestUri = "https://eventsite/api/" + id;
    var result = await httpClient.GetAsync(requestUri);
    var response = await result.Content.ReadAsStringAsync();
    var eventJObj = JsonConvert.DeserializeObject<JObject>(response);
    var evt = new Event
    {
        Name = eventJObj.Value<string>("name"),
        Time = DateTimeOffset.FromUnixTimeSeconds(eventJObj.Value<long>("time")).DateTime,
        Url = "https://eventsite/" + id
    };
    return PartialView("Event", evt);
}

The test is a tedious verification of every property on the event object. There are several ways to get around that. Amongst others, equality members. I've got a way better trick, but that's for an upcoming post.

Now that we're through those tests, let's dive into how we can test the actual HTML output of this whole shenanigan.

Generating some Razor

As mentioned initially, RazorGenerator is a hidden gem in the ASP.NET OSS wilderness. There's a Visual Studio plugin that you need to exploit it fully. It's aptly called RazorGenerator and can be installed from here. There's also a couple of Nuget packages that we want:

  • RazorGenerator.Mvc
    Referenced in your website to make use of a special ViewEngine for pre-compiled views.
  • RazorGenerator.Testing
    Referenced in our test projects to be able to automagically render views in tests.

Armed with these tools, there's nothing left to do but add a custom tool to our view:

RazorGenerator as Custom Tool for a view

As soon as you've blured the "Custom Tool" field, a .cs file will be generated beneath the view. Upon inspection it yields a class in the ASP namespace making a whole lot of Write* statements. This is what clutters up your ASP.NET Temporary files all the time. And the nice part: it can be instantiated.

Here's the initial markup from Event.cshtml:

@model Example.WebSite.Models.Event

<div>
    <a href="@Model.Url">
        <label>
            @Model.Name
        </label>
        <span>
            @Model.Time.ToString("f")
        </span>
    </a>
</div>

Over in our unit test, we can now start adding some actual rendering of HTML:

[Test]
[DocumentationOrder(2)]
public void It_Is_Rendered_With_A_Name_Date_And_A_Link()
{
    var view = new _Views_Partials_Event_cshtml();
    var result = GetConcertEvent();

    Assert.AreEqual("Event", result.ViewName);

    var renderedResult = view.Render((Event) result.Model);

    Console.WriteLine(renderedResult);
    Assert.Inconclusive("Rendering has not been implemented yet.");
}

private PartialViewResult GetConcertEvent()
{
    httpMessageHandler.Register(
        "https://eventsite/api/123",
        "{\"name\":\"Concert of your life\", \"time\":2524607999}"
    );

    var result = eventController.Event(123).Result;
    return result;
}

I know, the view type name ain't too pretty. But it's necessary for the view engine to find the right one based on the path given. The cool part is the Render statement. It's an extension method from the RazorGenerator.Tests package. It returns the final HTML as a string.

The Console.WriteLine statement yields the following in our test output now:

<div>
    <a href="https://eventsite/123">
        <label>
            Concert of your life
        </label>
        <span>
            fredag 31. desember 2049 23.59
        </span>
    </a>
</div>

(Yes, I force my locale upon thee!)

What we've just done is to test our system almost end to end, cut off at the uttermost borders to external IO. Specifically, the third party site and the end-user's browser.

Granted, we could do Selenium tests here to really test it via the browser, but my rule of thumb is that RazorGenerator is the best choice unless you've got a lot of JavaScript interactivity you need to test in integration. These are subjects for another post.

There is a remaining issue though. We should assert the HTML we got rendered. We could store that HTML we got in a const string expected in our test, but it's gonna foul up the code quite a bit. We could go with so-called "gold files" and implement something to compare a file to the actual output. There's a magical tool for that called ApprovalTests, which I'll cover in my next post.

There's also the option of using HtmlAgilityPack to query the rendered view. The RazorGenerator.Tests package have built-in alternative for Render called RenderAsHtml that returns HtmlDocument instances for you to query. It's quite useful when your only Assert.That is for some specific element in a big page.

Resources and a small limitation

You've seen how you can use RazorGenerator to test your views. There are several posts by David Ebbo (one of the authors of RazorGenerator) on how to use RazorGenerator. Please check them out for more details than I was able to give here.

For now it doesn't do nested Html.Action or Html.Partial calls. I've got a PR in the works, but I need to polish it for it to get in there. Some day soon. ;) If you really want to, you'll find my fork and build your own, but you'll be on your own.

There's also a tiny performance hit. You'll have to wait a second longer for your tests to execute, since a lot of the ASP.NET MVC framework is spun up to render views. It's still less than the magical 2 second focus cap though, so you should be able to work effectively with it.

I hope this piqued your interest in writing broader tests even up to the UI layers. There's even cooler tricks in store for you if you're already on .net Core, but the rest of us will have to make due until then.

Author

comments powered by Disqus