如何使用MVC ASP.Net对正确的视图进行单元测试?

时间:2021-06-13 01:44:47

I’m new to MVC, Unit Testing, Mocking and TDD. I’m trying to follow best practice as closely as possible.

我是MVC,单元测试,mock和TDD的新手。我试着尽可能地遵循最佳实践。

I’ve written a unit test for a controller and I’m having trouble testing if the correct view is returned. If I use the ViewResult.ViewName the test always fails if I don’t specify the view name in the controller. If I do specify the ViewName in the controller the test always passes, even if the view doesn’t exist.

我已经为控制器编写了一个单元测试,如果返回正确的视图,就会遇到麻烦。如果我使用ViewResult。如果我不在控制器中指定视图名,测试总是失败。如果我在控制器中指定了ViewName,那么测试将始终通过,即使该视图不存在。

I’ve also tried testing the Response.Status code however this always returns 200 (code taken from Darin Dimitrov’s answer to MVC3 unit testing response code). What I’m aiming for is the classic red, green refactor when creating a new view and avoiding 404 and System.InvalidOperationException errors when going live, is this possible?

我也尝试过测试响应。但是状态码总是返回200(来自Darin Dimitrov对MVC3单元测试响应代码的回答)。我的目标是在创建新视图和避免404和系统时采用经典的红色、绿色重构。当运行时,是否可能出现InvalidOperationException错误?

Code Below.

下面的代码。

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);
    }
}

2 个解决方案

#1


25  

I’m new to MVC, Unit Testing, Mocking and TDD. I’m trying to follow best practice as closely as possible.

我是MVC,单元测试,mock和TDD的新手。我试着尽可能地遵循最佳实践。

I feel happy that more and more developers are starting to write unit tests for their code, so congratulations you are on the right path.

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

if I don’t specify the view name in the controller. If I do specify the ViewName in the controller the test always passes, even if the view doesn’t exist.

如果我没有在控制器中指定视图名。如果我在控制器中指定了ViewName,那么测试将始终通过,即使该视图不存在。

When you do not specify a view name in the View method this instructs the MVC engine to render the default view, so for example

当不在视图方法中指定视图名时,这将指示MVC引擎呈现默认视图,例如

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

The above code will return an empty view name meaning that the rendered view will be the name of the action, in this case it will be Index.

上面的代码将返回空视图名,这意味着呈现的视图将是操作的名称,在本例中它将是索引。

So if you want to test that an action returns the default view, you have to test that the returned view name is empty

因此,如果您想测试一个操作是否返回默认视图,您必须测试返回的视图名称是否为空

Test always passes as view name is specified even if the view doesn't exist.

即使视图不存在,当指定视图名时,测试也始终通过。

In order to explain what's going on here, I'll explain first how the action filters work.

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

There are basically four types of filters

基本上有四种类型的过滤器

  • Exception filters
  • 异常过滤器
  • Authorization filters
  • 授权的过滤器
  • Action filters
  • 行动过滤器
  • Result filters
  • 结果过滤器

I will concentrate on action and result filters

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

Action filters are defined with the IActionFilter interface

动作过滤器是用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);
}

Result filters are defined with the IResultFilter interface

使用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);
}

When a controller's action is executed the following filters are executed in this specific order:

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

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

When an action is executed, another component is in charge to process your ActionResult returned from your action and render the correct HTML to send it back to the client, this is when the result is processed

当执行一个操作时,另一个组件负责处理从操作返回的ActionResult,并呈现正确的HTML以将其发送回客户机,这是处理结果的时候

This clean separation of concerns is the beauty and the key to allow us unit test our controller's actions, otherwise, if they were coupled, we won't be able to unit test in isolation the result of the actions

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

Now the RazorViewEngine tries to find a view after an action has been executed (when the result is being processed) that's why your tests return true even when the physical view does not exist. This is the expected behavior, and remember you need to test in isolation your controller's actions. As long as you assert in your unit tests that the expected view is rendered you are done with your unit tests.

现在,RazorViewEngine尝试在一个操作执行之后(当结果被处理时)找到一个视图,这就是为什么即使物理视图不存在,您的测试仍然返回true。这是预期的行为,请记住,您需要隔离地测试控制器的操作。只要您在单元测试中断言所期望的视图被呈现,您就完成了单元测试。

If you want to assert that the physical view exists then you would be talking about some specific integration tests: functional tests or user acceptance tests - these kind of tests require the instantiation of your application using a browser they are not in any way unit tests

如果您想断言物理视图存在,那么您将讨论一些特定的集成测试:功能测试或用户接受测试——这些测试需要使用浏览器实例化应用程序——它们在任何方面都不是单元测试

Now it's OK that you are writing your unit tests manually (this is a great exercise if you are getting into the unit testing world), however, I'd like to recommend you a couple of MVC Testing frameworks that can help you write your unit tests really fast

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

A few personal comments about these frameworks

关于这些框架的一些个人评论

According to my experience, MVC Contrib has more features than Fluent MVC Testing, however, since I'm using MVC 4 I have not been able to get it working in Visual Studio 2012, so I use a combination of both (this is a dirty workaround until I find a better approach)

根据我的经验,MVC后悔器比Fluent MVC测试有更多的特性,但是,由于我使用的是MVC 4,所以我无法在Visual Studio 2012中使用它,所以我将两者结合使用(这是一个肮脏的解决方案,直到我找到更好的方法)

This is what I do:

这就是我所做的:

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

I hope this helps =) Happy coding !

我希望这有助于)快乐的编码!

#2


0  

Another good option are the MVC extensions for Fluent Assertions.

另一个不错的选择是为Fluent断言提供MVC扩展。

#1


25  

I’m new to MVC, Unit Testing, Mocking and TDD. I’m trying to follow best practice as closely as possible.

我是MVC,单元测试,mock和TDD的新手。我试着尽可能地遵循最佳实践。

I feel happy that more and more developers are starting to write unit tests for their code, so congratulations you are on the right path.

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

if I don’t specify the view name in the controller. If I do specify the ViewName in the controller the test always passes, even if the view doesn’t exist.

如果我没有在控制器中指定视图名。如果我在控制器中指定了ViewName,那么测试将始终通过,即使该视图不存在。

When you do not specify a view name in the View method this instructs the MVC engine to render the default view, so for example

当不在视图方法中指定视图名时,这将指示MVC引擎呈现默认视图,例如

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

The above code will return an empty view name meaning that the rendered view will be the name of the action, in this case it will be Index.

上面的代码将返回空视图名,这意味着呈现的视图将是操作的名称,在本例中它将是索引。

So if you want to test that an action returns the default view, you have to test that the returned view name is empty

因此,如果您想测试一个操作是否返回默认视图,您必须测试返回的视图名称是否为空

Test always passes as view name is specified even if the view doesn't exist.

即使视图不存在,当指定视图名时,测试也始终通过。

In order to explain what's going on here, I'll explain first how the action filters work.

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

There are basically four types of filters

基本上有四种类型的过滤器

  • Exception filters
  • 异常过滤器
  • Authorization filters
  • 授权的过滤器
  • Action filters
  • 行动过滤器
  • Result filters
  • 结果过滤器

I will concentrate on action and result filters

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

Action filters are defined with the IActionFilter interface

动作过滤器是用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);
}

Result filters are defined with the IResultFilter interface

使用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);
}

When a controller's action is executed the following filters are executed in this specific order:

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

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

When an action is executed, another component is in charge to process your ActionResult returned from your action and render the correct HTML to send it back to the client, this is when the result is processed

当执行一个操作时,另一个组件负责处理从操作返回的ActionResult,并呈现正确的HTML以将其发送回客户机,这是处理结果的时候

This clean separation of concerns is the beauty and the key to allow us unit test our controller's actions, otherwise, if they were coupled, we won't be able to unit test in isolation the result of the actions

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

Now the RazorViewEngine tries to find a view after an action has been executed (when the result is being processed) that's why your tests return true even when the physical view does not exist. This is the expected behavior, and remember you need to test in isolation your controller's actions. As long as you assert in your unit tests that the expected view is rendered you are done with your unit tests.

现在,RazorViewEngine尝试在一个操作执行之后(当结果被处理时)找到一个视图,这就是为什么即使物理视图不存在,您的测试仍然返回true。这是预期的行为,请记住,您需要隔离地测试控制器的操作。只要您在单元测试中断言所期望的视图被呈现,您就完成了单元测试。

If you want to assert that the physical view exists then you would be talking about some specific integration tests: functional tests or user acceptance tests - these kind of tests require the instantiation of your application using a browser they are not in any way unit tests

如果您想断言物理视图存在,那么您将讨论一些特定的集成测试:功能测试或用户接受测试——这些测试需要使用浏览器实例化应用程序——它们在任何方面都不是单元测试

Now it's OK that you are writing your unit tests manually (this is a great exercise if you are getting into the unit testing world), however, I'd like to recommend you a couple of MVC Testing frameworks that can help you write your unit tests really fast

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

A few personal comments about these frameworks

关于这些框架的一些个人评论

According to my experience, MVC Contrib has more features than Fluent MVC Testing, however, since I'm using MVC 4 I have not been able to get it working in Visual Studio 2012, so I use a combination of both (this is a dirty workaround until I find a better approach)

根据我的经验,MVC后悔器比Fluent MVC测试有更多的特性,但是,由于我使用的是MVC 4,所以我无法在Visual Studio 2012中使用它,所以我将两者结合使用(这是一个肮脏的解决方案,直到我找到更好的方法)

This is what I do:

这就是我所做的:

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

I hope this helps =) Happy coding !

我希望这有助于)快乐的编码!

#2


0  

Another good option are the MVC extensions for Fluent Assertions.

另一个不错的选择是为Fluent断言提供MVC扩展。