Llewellyn Falco and I had a conversation many years ago (June 2010?) about the best way to test Web UI. During that conversation we referred to the two classifications/mechanisms of web testing as front-door and back-door web testing. That is how I still think of the two types many years later, although I recognize that not many people in the industry use those terms.

In front-door web testing you are using the browser to drive the test, which more closely tests what the user sees, but offers limited ability to manipulate or control the data and other dependencies. The other drawback of this type of testing is if the test modifies data, there needs to be some way to get back to a clean slate after the test finishes.

In back-door web testing you call the controller or presenter directly (assumes you are using MVC pattern, or have done a good job separating the Greedy view into a presenter). The advantage of this pattern is that you can control the dependencies and data context under which the test runs more easily by using in memory repositories, mocks, and things of that nature. The main issue with this type of testing is that these controller methods return some sort of model and view name, making it difficult to test what the user sees. Because of this, you can have complete test coverage over the controllers but still have bugs in the view.

In January of 2011 ASP.NET MVC 3 was released which allowed different view engines to be used to render the views into HTML that would be sent back to the client. Because the View engines were easily pluggable and the Razor Engine was packaged separately this allowed back door testing to call the engine to produce HTML. This allowed back-door web testing to get closer to what the user was seeing and eventually resulted in Llewellyn augmenting Approval tests with a mechanism for Approving HTML.

However, there are still problems with this approach. Two of the biggest problems are

  1. changes to the layout template break all tests
  2. inability to test JavaScript manipulations of the page

This blog was cross posted on the Crafting Bytes blog at Web UI Testing Part 1: Front-door and back-door testing

When I started doing more complicated things with ASP.NET MVC it was using Razor. In some ways that was unfortunate because some of these things were actually a little easier in prior versions. It starts to get complicated when you start composing partial views and multiple javascript files. First some Javascript files depend on other javascript files. And secondly partial views need certain scripts to be included that the main page doesn’t necessarily know about. The problem is that Razor doesn’t really deal with these things very well.

For this blog entry I am going to focus on getting JavaScript files included from partial views. This question has been asked numerous times on Stack Overflow

  • http://stackoverflow.com/questions/863436/is-it-bad-practice-to-return-partial-views-that-contain-javascript
  • http://stackoverflow.com/questions/912755/include-javascript-file-in-partial-views
  • http://stackoverflow.com/questions/4707982/how-to-include-javasscript-from-a-partial-view-in-asp-net-mvc3
  • http://stackoverflow.com/questions/5376102/mvc-partial-views-and-unobtrusive-jquery-javascript
  • http://stackoverflow.com/questions/7556400/injecting-content-into-specific-sections-from-a-partial-view-asp-net-mvc-3-with
  • http://stackoverflow.com/questions/11098198/is-it-ok-to-put-javascript-in-partial-views

In fact, I bet if you put all of the questions in 1 it would have quite a point total. But it is spread into so many slightly different questions that it is tough to quantify.

So first to define the problem. The ideal place for scripts is right before the close of the body tag. The default template’s master/layout view contains a scripts section for this purpose. Unfortunately sections can only be defined, not added to. So that means that the main view is the only one that can place script files in that section. It can get very awkward if there are script files that are very specific to the partial view, especially if the main view includes a number of partials. Basically the master view has to maintain the list of scripts needed by the entire tree of partial views.

Let’s make the problem more concrete. Let’s say I have three main view that include a partial view. That partial view uses another partial view. I change the leaf partial view so that I need some JavaScript. I have to find out where all of the views are that include me (but of course no view includes me directly), and add the script to those views. In short – YUCK.

While researching a solution to the problem, I came across a couple of promising solutions, namely:

http://stackoverflow.com/questions/5433531/using-sections-in-editor-display-templates/

And

http://forloop.co.uk/blog/managing-scripts-for-razor-partial-views-and-templates-in-asp.net-mvc

The first didn’t take into account paths, and the second was way too complicated in terms of how to use them, so I came up with this nice simple hybrid of the two solutions.

Here is an example of its use
Either at the top of the file or the web config, need to use the namespace

@using PartialsWithScripts.Helpers

To include a script in a partial view simple add it like so:

@{ Html.MyAddScriptFile("~/Scripts/App/contact.js"); }

Here is the code

public static class ScriptHelpers
{
    const string ScriptContextKey = "ScriptContext";

    public static void AddScript(this HtmlHelper htmlHelper, string path)
    {
        var scriptContext = GetScriptContext(htmlHelper);
        scriptContext.Add(path);
    }

    public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
    {
        var httpContext = htmlHelper.ViewContext.HttpContext;
        var scriptContext = httpContext.Items[ScriptContextKey] as HashSet<string>;
        if (scriptContext != null)
        {
            var builder = new StringBuilder();
            var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext,
                                htmlHelper.RouteCollection);
            foreach (var scriptFile in scriptContext)
            {
                builder.AppendLine("<script type='text/javascript' src='" 
                    + urlHelper.Content(scriptFile) + "'></script>");
            }
            return new MvcHtmlString(builder.ToString());
        }
        return MvcHtmlString.Empty;
    }

    private static HashSet<string> GetScriptContext(HtmlHelper htmlHelper)
    {
        var httpContext = htmlHelper.ViewContext.HttpContext;
        var scriptContext = httpContext.Items[ScriptContextKey] as HashSet<string>;
        if (scriptContext == null)
        {
            scriptContext = new HashSet<string>();
            htmlHelper.ViewContext.HttpContext.Items[ScriptContextKey] = scriptContext;
        }
        return scriptContext;
    }
}