10

我是 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);
    }
}
4

1 回答 1

26

我是 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");

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

于 2012-11-08T23:41:42.293 回答