【问题标题】:How do I Unit Test the correct view is returned with MVC ASP.Net?如何对 MVC ASP.Net 返回正确的视图进行单元测试?
【发布时间】:2012-10-29 05:24:07
【问题描述】:

我是 MVC、单元测试、模拟和 TDD 的新手。我正在尝试尽可能地遵循最佳实践。

我为控制器编写了一个单元测试,如果返回正确的视图,我无法测试。如果我使用 ViewResult.ViewName 如果我没有在控制器中指定视图名称,那么测试总是会失败。如果我确实在控制器中指定了 ViewName,测试总是通过,即使视图不存在。

我也尝试过测试 Response.Status 代码,但它总是返回 200(代码取自 Darin Dimitrov 对 MVC3 unit testing response code 的回答)。我的目标是在创建新视图时进行经典的红绿重构,并在上线时避免 404 和 System.InvalidOperationException 错误,这可能吗?

代码如下。

public class BugStatusController : Controller
{
    public ActionResult Index(){
        return View(); // Test always fails as view name isn’t specified even if the correct view is returned.
    }

    public ActionResult Create(){
        return View("Create"); // Test always passes as view name is specified even if the view doesn’t exist.
    }
}

[TestFixture]
public class BugStatusTests
{    
    private ViewResult GetViewResult(Controller controller, string controllerMethodName){
        Type type = controller.GetType();
        ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);

        object instance = constructor.Invoke(new object[] {});
        MethodInfo[] methods = type.GetMethods();

        MethodInfo methodInfo = (from method in methods
                                where method.Name == controllerMethodName
                                                    && method.GetParameters().Count() == 0
                                select method).FirstOrDefault();

        Assert.IsNotNull(methodInfo, "The controller {0} has no method called {1}", type.Name, controllerMethodName);

        ViewResult result = methodInfo.Invoke(instance, new object[] {}) as ViewResult;

        Assert.IsNotNull(result, "The ViewResult is null, controller: {0}, view: {1}", type.Name, controllerMethodName);

        return result;
    }

    [Test]
    [TestCase("Index", "Index")]
    [TestCase("Create", "Create")]
    public void TestExpectedViewIsReturned(string expectedViewName, string controllerMethodName){
        ViewResult result = GetViewResult(new BugStatusController(), controllerMethodName);

        Assert.AreEqual(expectedViewName, result.ViewName, "Unexpected view returned, controller: {0}, view: {1}", CONTROLLER_NAME, expectedViewName);
    }

    [Test]
    [TestCase("Index", "Index")]
    [TestCase("Create", "Create")]
    public void TestExpectedStatusCodeIsReturned(string expectedViewName, string controllerMethodName)
    {
        var controller = new BugStatusController();
        var request = new HttpRequest("", "http://localhost:58687/", "");
        var response = new HttpResponse(TextWriter.Null);
        var httpContext = new HttpContextWrapper(new HttpContext(request, response));
        controller.ControllerContext = new ControllerContext(httpContext, new RouteData(), controller);

        ActionResult result = GetViewResult(controller, controllerMethodName);

        Assert.AreEqual(200, response.StatusCode, "Failed to load " + expectedViewName + " Error: "  + response.StatusDescription);
    }
}

【问题讨论】:

  • 如果您的操作未返回命名视图,则您通过断言该视图名称为空来验证该行为。如果操作返回一个命名视图,则断言视图名称存在于视图结果中。我不明白这种方法有什么问题?

标签: c# asp.net-mvc unit-testing tdd


【解决方案1】:

我是 MVC、单元测试、模拟和 TDD 的新手。我正在尝试尽可能地遵循最佳实践。

我很高兴越来越多的开发人员开始为他们的代码编写单元测试,所以恭喜你走在了正确的道路上。

如果我没有在控制器中指定视图名称。如果我确实在控制器中指定了 ViewName,则测试总是通过,即使视图不存在。

当您View 方法中未指定视图名称时,这会指示 MVC 引擎呈现默认视图,例如

public ActionResult Index() { return View(); }

上面的代码将返回一个空视图名称,这意味着渲染的视图将是动作的名称,在这种情况下它将是 Index

所以如果你想测试一个动作是否返回默认视图,你必须测试返回的视图名称是否为空

即使视图不存在,测试也总是通过指定视图名称。

为了解释这里发生了什么,我将首先解释操作过滤器的工作原理。

过滤器基本上有四种类型

  • 异常过滤器
  • 授权过滤器
  • 动作过滤器
  • 结果过滤器

我将专注于操作和结果过滤器

动作过滤器使用IActionFilter 接口定义

public interface IActionFilter
{
    // Summary:
    //     Called after the action method executes.
    //
    void OnActionExecuted(ActionExecutedContext filterContext);
    //
    // Summary:
    //     Called before an action method executes.
    //
    void OnActionExecuting(ActionExecutingContext filterContext);
}

结果过滤器使用IResultFilter 接口定义

public interface IResultFilter
{
    // Summary:
    //     Called after an action result executes.
    //
    void OnResultExecuted(ResultExecutedContext filterContext);
    //
    // Summary:
    //     Called before an action result executes.
    //
    void OnResultExecuting(ResultExecutingContext filterContext);
}

当执行控制器的操作时,以下过滤器会按此特定顺序执行:

IActionFilter.OnActionExecuting
IActionFilter.OnActionExecuted
IResultFilter.OnResultExecuting
IResultFilter.OnResultExecuted

当一个动作被执行时,另一个组件负责处理你从你的动作返回的ActionResult呈现正确的HTML发送回客户端 ,这是处理结果的时间

这种清晰的关注点分离是让我们对控制器的动作进行单元测试的美妙之处和关键,否则,如果它们是耦合的,我们将无法单独对动作的结果进行单元测试

现在RazorViewEngine 尝试在执行操作后(处理结果时)查找视图,这就是为什么即使物理视图不存在,您的测试也会返回 true。这是预期的行为,请记住您需要单独测试控制器的操作。只要您在单元测试中断言预期的视图已呈现,您就完成了单元测试。

如果您想断言物理视图存在,那么您将谈论一些特定的集成测试:功能测试或用户验收测试 - 这类测试需要使用浏览器实例化您的应用程序它们不是以任何方式进行单元测试

现在您可以手动编写单元测试(如果您正在进入单元测试世界,这是一个很好的练习),但是,我想向您推荐几个可以帮助您的 MVC 测试框架快速编写单元测试

关于这些框架的一些个人经验

根据我的经验,MVC Contrib 具有比 Fluent MVC Testing 更多的功能,但是,由于我使用的是 MVC 4,我无法让它在 Visual Studio 2012 中运行,所以我将两者结合使用(这个在我找到更好的方法之前,这是一个肮脏的解决方法)

这就是我的工作:

var testControllerBuilder = new TestControllerBuilder(); // this is from MVC Contrib
var controller = new MoviesController(
    this.GetMock<IMovieQueryManager>().Object);

testControllerBuilder.InitializeController(controller); // this allows me to use the Session, Request and Response objects as mock objects, again this is provided by the MVC Contrib framework

// I should be able to call something like this but this is not working due to some problems with DLL versions (hell DLL's) between MVC Controb, Moq and MVC itself
// testControllerBuilder.CreateController<MoviesController>();

controller.WithCallTo(x => x.Index(string.Empty)).ShouldRenderDefaultView(); // this is using Fluent MVC Testing

// again instead of the above line I could use the MVC Contrib if it were working....
// var res = sut.Index(string.Empty);
// res.AssertViewRendered().ForView("Index");

我希望这会有所帮助 =) 编码愉快!

【讨论】:

  • 非常棒的帖子!学到的东西比我搜索的要多!
  • 如果我理解正确,那么我不应该测试 viewName 是否为“index”。我应该断言 viewname 是空的?
  • 当你想要断言默认视图时返回yes。如果不是默认视图,则断言使用视图名称返回正确的视图
  • 要断言视图名称返回特定视图,您需要执行以下操作: var result = (ViewResult)result; Assert.AreEqual("viewName", result.ViewName);
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-24
相关资源
最近更新 更多