如何对控制器方法的 HandleError 属性的行为进行单元测试?
我正在尝试验证 ASP.Net MVC 应用程序在发生意外错误时的行为。 具体来说,我试图验证用户是否被定向到我为我的应用程序定义的错误页面。 我遇到的问题是我无法按预期验证控制器方法的行为。
对于我的正常行为测试,我创建一个模拟业务规则对象并将其传递给我的控制器,然后从我想要测试的控制器方法验证 ViewResult。 当事情按预期进行时,这对我的目的来说效果很好。 但是,当我从业务规则方法抛出异常时,该异常将通过控制器方法结果进行处理,而不是被处理(控制器方法具有“HandleError”属性)由控制器执行,以便返回适合我的错误页面的 ViewResult。
有没有办法以这种方式验证 HandleError 属性的行为? 还是我的做法完全错误? 我意识到我可以使用 Selenium(会命中实际服务器的浏览器内测试)来验证实际浏览器中的行为,但是模拟此类测试可以让我更快地完成此操作,并且开销要少得多...
示例测试代码:
// WidgetController.Index() makes a call to GetWidgets to retrieve a
// List<Widget> instance.
// this works as expected since the appropriate ViewResult is returned
// by the controller
public void TestWidgetControllerIndex_NoResultsFound()
{
var mockBR = new Mock<IBusinessRules> { CallBase = true };
mockBR.Setup(br=>fr.GetWidgets()).Returns(new List<Widget>());
WidgetController controller = new WidgetController(mockBR.Object);
ViewResult result = (ViewResult)controller.Index();
Assert.AreEqual("Index", result.ViewName);
Assert.AreEqual(0,
((WidgetIndexViewData)result.ViewData.Model).Widgets.Count);
}
// this test is unable to reach the assertion statements due to the problem
// outlined above. WidgetController.Index has the HandleError attribute
// properly applied and the behaviour via the interface is as expected
public void TestWidgetControllerIndex_BusinessRulesExceptionEncountered()
{
var mockBR = new Mock<IBusinessRules> { CallBase = true };
mockBR.Setup(br=>fr.GetWidgets()).Throws<ApplicationException>();
WidgetController controller = new WidgetController(mockBR.Object);
ViewResult result = (ViewResult)controller.Index();
// The ApplicationException thrown by the business rules object bubbles
// up to the test through the line above. I was expecting this to be
// caught and handled by the HandleError filter (which would then let
// me verify the behaviour results via the assertion below)..
Assert.AreEqual("Error", result.ViewName);
}
对于我可能做错了什么或者我是否只是从完全错误的方向接近这个问题的任何建议,我将不胜感激。 我假设在控制器方法级别进行测试是合适的方法,因为这是应用 HandleError 属性的地方。(如果我确实需要在应用程序级别进行测试,是否可以通过类似的方法来做到这一点实例化对象而不是使用像 Selenium 这样的东西?)
更新 我得出的结论是,我不应该在每个控制器操作上测试与 HandleError 属性相关的功能。 我实际上并不关心它的作用,我只是想确保错误得到处理(从我的测试角度来看,无论它的自定义代码还是 MVC 库都没有什么区别,这是我想要验证的功能)。
我最终所做的是将控制器操作包装在 try/catch 块中,以便强制返回 Error 视图作为方法的结果(而不是 ErrorHandler 属性在错误离开方法时捕获错误)。 这样我就可以在单元测试中断言错误已通过适当的反馈得到正确处理。 我对这添加到我的控制器方法中的额外长度不太满意,但它确实让我可以向用户提供友好的、特定的错误消息(我正在使用扩展方法来显示反馈并执行日志记录)。 (所以 try/catch 方法肯定有优点和缺点..)
我不是 100% 肯定这是最干净的方法,但它实现了我的目标,即能够验证错误是否通过控制器处理单元测试(快)而不是必须在浏览器中执行测试(慢)。 所以基本上现在足够好,直到我找到一个更干净的解决方案。 如果有人遇到类似问题并找到更好的解决方案,我决定提供赏金。
I'm trying to verify the behaviour of my ASP.Net MVC app when an unexpected error occurs. Specifically, I'm trying to verify that the user is directed to the error page I've defined for my app. The problem I'm encountering is that I'm not able to verify the behaviour of the controller method as expected.
For my normal behaviour tests, I create a mock business rule object and pass that to my controller and then verify the ViewResult from the controller method that I want to test. This works fine for my purposes when things work as expected. However, when I throw an exception from the business rule method, the exception is carried up through the controller method result rather than being handled (the controller method has the 'HandleError' attribute) by the controller so that an appropriate ViewResult for my error page being returned.
Is there any way to verify the behaviour of the HandleError attribute in this fashion? Or am I going about this completely wrong? I realize I could use Selenium (in-browser testing which would hit the actual server) to verify the behaviour in an actual browser, but mocking these sort of tests lets me do this faster and with much less overhead...
Sample Test Code :
// WidgetController.Index() makes a call to GetWidgets to retrieve a
// List<Widget> instance.
// this works as expected since the appropriate ViewResult is returned
// by the controller
public void TestWidgetControllerIndex_NoResultsFound()
{
var mockBR = new Mock<IBusinessRules> { CallBase = true };
mockBR.Setup(br=>fr.GetWidgets()).Returns(new List<Widget>());
WidgetController controller = new WidgetController(mockBR.Object);
ViewResult result = (ViewResult)controller.Index();
Assert.AreEqual("Index", result.ViewName);
Assert.AreEqual(0,
((WidgetIndexViewData)result.ViewData.Model).Widgets.Count);
}
// this test is unable to reach the assertion statements due to the problem
// outlined above. WidgetController.Index has the HandleError attribute
// properly applied and the behaviour via the interface is as expected
public void TestWidgetControllerIndex_BusinessRulesExceptionEncountered()
{
var mockBR = new Mock<IBusinessRules> { CallBase = true };
mockBR.Setup(br=>fr.GetWidgets()).Throws<ApplicationException>();
WidgetController controller = new WidgetController(mockBR.Object);
ViewResult result = (ViewResult)controller.Index();
// The ApplicationException thrown by the business rules object bubbles
// up to the test through the line above. I was expecting this to be
// caught and handled by the HandleError filter (which would then let
// me verify the behaviour results via the assertion below)..
Assert.AreEqual("Error", result.ViewName);
}
I'd appreciate any suggestions as to what I might be doing wrong or whether I'm just approaching this from entirely the wrong direction. I'm making the assumption that testing at the controller method level is that appropriate way to go here since that's where the HandleError attribute is applied.. ( If I do need to test at the application level, is it possible to do that via similar instantiated objects rather than using something like Selenium? )
Update
I've come to the conclusion that I shouldn't be testing the functionality related to the HandleError attribute on each controller action. I don't actually care about what it does, I just want to make sure that the error is handled (from my test perspective whether its custom code or the MVC libraries doesn't make a difference, it's the functionality that I want to verify).
What I've ended up doing is wrapping my controller actions in try/catch blocks in order to force the Error view to be returned as a result of the method (rather than the ErrorHandler attribute catching the error as it leaves the method). This way I can assert in my unit tests that the error is properly handled with appropriate feedback. I'm not very pleased with the extra length that this adds to my controller methods, but it does let me provide a friendly, specific error message to the user (I'm using an extension method to display the feedback and perform logging). (So there are pros and cons to the try/catch approach for sure..)
I'm not 100% positive that this is the cleanest way to go, but it achieves my goal of being able to verify that errors are handled via controller unit-tests (fast) rather than having to execute tests in a browser (slow). So basically it's good enough for now, until I can find a cleaner solution. I've decided to offer a bounty if anyone encounters a similar problem and has found a better solution..
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我认为你不能对此进行单元测试。 你也不想。 您可以测试控制器方法是否引发预期的异常。 使用反射,您可以测试操作或控制器是否具有您期望它具有的属性,以及该属性是否具有某些预期的属性值。 但是,拦截异常并执行属性的工作是框架的行为,而不是您的代码。 一般来说,您不应该测试不属于您的代码(框架)。
I think you cannot unit test this. Nor do you want to. You can test that the expected exception is thrown by a controller method. Using reflection you can test that the action or controller has the attribute that you expect it to have and that the attribute has certain expected property values. However, the job of intercepting the exception and executing the attribute is behavior of the framework, not your code. Generally speaking you should not test code that is not yours (the framework).
是的,我同意蒂姆的观点。 您所描述的是集成测试。 HandleErrorAttribute 的行为取决于底层 ASP.NET 行为,例如您在 web.config 中是否打开或关闭了自定义错误。
对于您的单元测试,您可以简单地使用反射来验证该属性是否存在。
MVC 团队对 HandleErrorAttribute 有单元测试,因此您不需要编写这些单元测试。 :) 然后只需手动测试您的操作以确保这是您想要的行为。 只要您从不更改/删除该属性,行为就不应该改变。
如果您想自动化集成测试,您可以使用 Watin 或 Selenium 来自动执行实际的浏览器请求,以确保您的应用程序行为不会改变。
Yes, I agree with Tim. What you're describing is an integration test. The HandleErrorAttribute's behavior depends on underlying ASP.NET behavior such as whether you have custom errors on or off in web.config.
For your unit test, you can simply use reflection to verify that the attribute is present.
The MVC team has unit tests for the HandleErrorAttribute, so you don't need to write those. :) Then just manually test your action to make sure that's the behavior you want. As long as you never change/remove the attribute, the behavior shouldn't change.
If you want to automate the integration test, you can use Watin or Selenium to automate an actual browser request to make sure your app behavior doesn't change.