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

Kev*_* L. 7 asp.net-mvc unit-testing razor vs-unit-testing-framework visual-studio-2013

如果我创建一个新的MVC 5项目(使用单元测试)并使用来自流行的SO答案的片段为我的控制器创建一个新的超类,则很容易将视图的内容呈现为字符串:

HomeController.cs

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

        return Content(result);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果我访问/Home/StringIndex,我会返回该视图的原始HTML.整洁(即使不是很有用)!但是在.Tests项目中,如果我尝试在单元测试中测试StringIndex()...

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"));
    }
}
Run Code Online (Sandbox Code Playgroud)

......没有这样的运气.调用controller.StringIndex()从单元测试创建ArgumentNullExceptionSystem.Web.Mvc.ViewEngineCollection.FindPartialView()被称为在上述的代码段,考虑的controllerContextnull.我已经尝试了几起订量为基础的办法(修改后的版本SetUpForTest()MvcMockHelpers)嘲笑了controllerContext,但是这可能是错误的做法,因为:1)这两种方法都不是专门针对Visual Studio中的单元测试,以及2)我不能完全肯定为了成功渲染视图,需要真实与模拟的内容.

在Visual Studio单元测试中是否可以创建一个能够让RenderRazorViewToString()工作的controllerContext

编辑澄清我的目标: 我不想测试RenderRazorViewToString()(这只是工作中使用的工具)的内部工作原理; 我希望我的单元测试能够分析在正常情况下从控制器返回的实际HTML.因此,如果(作为一个坏的,愚蠢的例子)我的Index.cshtml只是<h2>@DateTime.Now.Year</h2>,那么Assert.IsTrue(resultString.Contains("<h2>2013</h2> "));(作为最后一行HomeControllerTest.StringIndex())将成功.

Spo*_*ock 2

您可以通过一些调整来测试此方法。为了测试这一点,您需要修改您的 SUT(被测系统),使其变得更易于测试。改变你的 SUT 总是一件好事,这样 API 就变得更容易测试,即使有时它看起来有点奇怪。

您的 SUT 中存在一些难以测试的罪魁祸首。A。

        using (var sw = new StringWriter())
Run Code Online (Sandbox Code Playgroud)

b. (在 RenderRazorViewToString 内部)

        ViewEngines.Engines.FindPartialView(ControllerContext, "Index");
Run Code Online (Sandbox Code Playgroud)

使用StringWriter,您需要能够获得测试启动的 StringWriter,以便您可以控制该编写器向视图写入的内容。

对于 FindPartialView,ViewEnginesCollection 是 ViewEngines 中的静态集合,FindPartialView 有很多事情发生在底层,而且似乎更难存根。可能还有另一种方法,因为 FindPartialView 是虚拟的,所以我们可以注入一个存根 ViewEngine,我们也许能够存根 FindPartialView 方法。但我并不处于存根/模拟整个宇宙的位置,所以我采取了不同的方法,但仍然达到了目的。这是通过引入委托来实现的,这样我就可以完全控制 FindPartialView 返回的内容。

被测系统 (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);
    }
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,有两个委托,一个用于 StringWriter,由 StringWriterFunc 调用,另一个用于 FindPartialView,由 ViewEngineResultFunc 调用。

在实际的程序执行期间,这些委托应该使用真实的实例,而在测试执行期间,它们将被假实例替换。

单元测试

[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;
    }
}
Run Code Online (Sandbox Code Playgroud)