我为控制器编写了单元测试,如果返回正确的视图,我无法测试.如果我使用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}",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_NAME,expectedViewName); } [Test] [TestCase("Index","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); } }
解决方法
I’m new to MVC,Unit Testing,Mocking and TDD. I’m trying to follow best practice as closely as possible.
我感到开心,越来越多的开发人员开始为他们的代码编写单元测试,所以恭喜你走在正确的道路上.
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.
当您在View方法中未指定视图名称时,将指示MVC引擎呈现默认视图,例如
public ActionResult Index() { return View(); }
上面的代码将返回一个空的视图名,这意味着渲染的视图将是操作的名称,在这种情况下它将是Index.
因此,如果要测试一个操作返回默认视图,则必须测试返回的视图名称是否为空
Test always passes as view name is specified even if the view doesn’t exist.
为了解释这里发生了什么,我将首先解释动作过滤器的工作原理.
基本上有四种类型的滤波器
>异常过滤器
>授权过滤器
>动作过滤器
>结果过滤器
我会专注于行动和结果过滤器
动作过滤器由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测试框架测试真的很快
> https://github.com/robdmoore/FluentMVCTesting
> http://mvccontrib.codeplex.com/wikipage?title=TestHelper&referringTitle=Documentation
关于这些框架的一些个人评论
根据我的经验,MVC Contrib具有比Fluent MVC测试更多的功能,由于我使用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");
我希望这有助于=)快乐的编码!