How can I render a view into a string in Visual Studio Unit Testing Framework?

If I create a new MVC 5 project (with unit tests) and make a new superclass for my controllers using a snippet from a popular SO answer it is easy to render the contents of a view into a string:

HomeController.cs

public class HomeController : StringableController
{
    public ActionResult StringIndex()
    {
        string result = RenderRazorViewToString("Index", null);

        return Content(result);
    }
}

Now if I visit /Home/StringIndex , I get back the raw HTML for that view. Neat (even if not very useful)! But over in the .Tests project, if I try to test StringIndex() in a unit test...

HomeControllerTest.cs

[TestClass]
public class HomeControllerTest
{
    [TestMethod]
    public void StringIndex()
    {
        HomeController controller = new HomeController();

        ContentResult result = controller.StringIndex() as ContentResult;
        string resultString = result.Content;

        Assert.IsTrue(resultString.Contains("Getting started"));
    }
}

...no such luck. Calling controller.StringIndex() from the unit test creates an ArgumentNullException when System.Web.Mvc.ViewEngineCollection.FindPartialView() is called in the aforementioned snippet, on account of controllerContext being null . I have tried a few Moq based approaches (modified versions of SetUpForTest() and MvcMockHelpers ) to mock up the controllerContext , but this may be the wrong approach, because 1) neither approach was specifically tailored to unit testing within Visual Studio, and 2) I am not entirely sure what needs to be real vs. mocked in order to successfully render the view.

Is it possible -in Visual Studio unit tests- to create a controllerContext that's capable of getting RenderRazorViewToString() to work?

EDIT to clarify my goal: I don't want to test the inner workings of RenderRazorViewToString() (which is just a tool being used for the job); I want my unit test to be able to analyze the actual HTML that would be returned from the controller in a normal case. So if (as a bad, silly example) my Index.cshtml is just <h2>@DateTime.Now.Year</h2> , then Assert.IsTrue(resultString.Contains("<h2>2013</h2> ")); (as the last line in HomeControllerTest.StringIndex() ) will succeed.


You can get this method to test with few tweaks. In order to test this, you need to modify your SUT (System Under Test), in such a way so it is become more testable. It is always a good thing to change you SUT so the API become more testable, even sometime it looks bit odd.

There are culprits within your SUT that harder to test. a.

        using (var sw = new StringWriter())

b. (inside RenderRazorViewToString)

        ViewEngines.Engines.FindPartialView(ControllerContext, "Index");

With the StringWriter you need to be able get hold of a test initiated StringWriter so you have the Control over what has been written to the View by that writer.

With FindPartialView, ViewEnginesCollection is a static collection in ViewEngines, and FindPartialView has lot of things happening underneated, and it seems harder to stub out. There might be another way since FindPartialView is virtual, so we can inject a stubbed ViewEngine we might be able to stub FindPartialView method. But I'm not in a position stub/mock the entire universe so I took a different approach but still serves the purpose. This is by introducing a delegate so I have full control over what has been returned by the FindPartialView.

System Under Test (SUT)

public class HomeController : Controller
{
    public Func<ViewEngineResult> ViewEngineResultFunc { get; set; }
    public Func<StringWriter> StringWriterFunc { get; set; }

    public HomeController()
    {
        ViewEngineResultFunc = () =>
        ViewEngines.Engines.FindPartialView(ControllerContext, "Index");
    }

    private string RenderRazorViewToString(string viewName, object model)
    {
        ViewData.Model = model;
        using (var sw = new StringWriter())
        {
            StringWriter stringWriter = StringWriterFunc == null ?
                            sw : StringWriterFunc();

            var viewResult = ViewEngineResultFunc();
            var viewContext = new ViewContext(ControllerContext, 
                viewResult.View, ViewData, TempData, stringWriter);
            viewResult.View.Render(viewContext, stringWriter);
            viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
            return stringWriter.GetStringBuilder().ToString();
        }
    }

    public ActionResult StringIndex()
    {
        string result = RenderRazorViewToString("Index", null);
        return Content(result);
    }

As you see there are two delegates one for the StringWriter, invoked by the StringWriterFunc and the other for the FindPartialView which is invoked by the ViewEngineResultFunc.

During the actual program execution these delegates should use the real instances, where as during the test execution the would replace by the fake instances.

Unit Test

[TestClass]
public class HomeControllerTest
{
    [TestMethod]
    public void StringIndex_RenderViewToString_ContentResuleContainsExpectedString()
    {
        //Arrange
        const string viewHtmlContent = "expectedViewContext";
        var sut = new HomeController();
        var sw = new StringWriter();
        var viewEngineResult = SetupViewContent(viewHtmlContent, sw);
        var controllerContext = new ControllerContext
          (new Mock<HttpContextBase>().Object, new RouteData(), 
          new Mock<ControllerBase>().Object);
        sut.ControllerContext = controllerContext;
        sut.ViewEngineResultFunc = () => viewEngineResult;
        sut.StringWriterFunc = () => sw;

        //Act
        var result = sut.StringIndex() as ContentResult;
        string resultString = result.Content;

        //Assert
        Assert.IsTrue(resultString.Contains(viewHtmlContent));
    }

    private static ViewEngineResult 
       SetupViewContent(string viewHtmlContent, StringWriter stringWriter)
    {
        var mockedViewEngine = new Mock<IViewEngine>();
        var resultView = new Mock<IView>();

        resultView.Setup(x => x.Render(It.IsAny<ViewContext>(), 
          It.IsAny<StringWriter>()))
              .Callback(() => stringWriter.Write(viewHtmlContent));
        var viewEngineResult = new ViewEngineResult
                (resultView.Object, mockedViewEngine.Object);

        ViewEngines.Engines.Clear();
        ViewEngines.Engines.Add(mockedViewEngine.Object);
        return viewEngineResult;
    }
}

An alternative approach to directly analyzing the HTML would be to use a test framework such as Selenium WebDriver (from coded unit tests) which will programmatically "drive" the page under test and you can then write your test assertions against the "page" using WebDriver to check for presence of elements, element values, etc.

There's a good write up of using it from unit tests with MVC and IIS Express here

链接地址: http://www.djcxy.com/p/18100.html

上一篇: 如何模拟Android设备上的网络问题?

下一篇: 如何在Visual Studio单元测试框架中将视图呈现为字符串?