In ASP. httpContext, the natural enemy of unit testing in. NET, is ASP. the core of NET is extremely complex, but Mock1 cannot be implemented. It can be seen that Microsoft can write such a huge ASP. NET Framework is really not that easy. Now this situation has improved a lot, so you can use System. Web. Abstractions. dll. This program provides an abstract for HttpContext, that is, the HttpContextBase abstract class. Therefore, in ASP. net mvc, various components depend on HttpContextBase rather than HttpContext. This is an excellent practice. You can try to get rid of HttpContext in the future.
However, this seems to be a paradox. Although Mock can be performed on HttpContext (which enhances testability), excessive dependency on HttpContext is also a harm to unit tests. This is because of the nature of the HttpContext object: It is too complicated. You should have noticed that this is a collection of favorite objects, from requests, replies, applications, caches ...... It contains almost all the information required by Web applications. To test a method that depends on HttpContext, you must fill in various information for the Mock object of HttpContext-the complexity depends on the business. In addition, Mock focuses on "behavior", that is, it focuses on the path used for doing one thing ". Then, if one thing can adopt multiple paths, how can we determine whether to prepare all the paths before the test and verify that the tested code is used, and uses only one of the paths. "Stub slowly enters the eyes of people. Stub is concerned with "status "...... This is another topic. It also involves using Record & Replay or Arrange-Act-Assert for unit testing.
When talking about unit tests on views, Lao Zhao once talked about using only the data in ViewData in views. This is not the first time that we have to give up HttpContext. Since the powerful weapon of "abstraction", all "discord" factors can be separated. In the MVP mode, View and Presenter interact with each other using their own abstractions. All Web controls, HttpContext, and other objects no longer exist. In everyone's eyes, there is only "data" and "model ". Similarly, HttpContext should not be used in the Action Method of ASP. net mvc, which is based on good testability. You may think that the current HttpContextBase object can be Mock. Yes, it does. But this will lead to the expansion of unit test code, because a considerable part of the test code must focus on the preparation of test data, rather than the function to be tested. For an Action method, it focuses on the interaction between users and business logic, rather than "how to convert HTTP requests into available data ". In fact, we still need to "separate concerns ".
In ASP. net mvc, the layer responsible for "converting data" is Model Binder. In this regard, most of the existing "Examples" Focus on converting data in Form or QueryString to the Action parameter, but Model Binder is actually more available. For example, in the "best practices" code, the Delete method of the original AccountController is implemented as follows:
Public ActionResult Delete (string userName)
{
This. MiddleTier. UserManager. Delete (userName );
Uri urlReferrer = this. Request. UrlReferrer;
Return this. Redirect (urlReferrer. ToString ());
}
After a specified object is deleted, the page will jump to the Url Referrer address. In the above Code, this value is obtained by accessing Request. UrlReferer. Therefore, your Action method is dependent on HttpContext. Therefore, you need to write the unit test code as follows:
[TestMethod]
Public void DeleteTest ()
{
String userName = "jeffz ";
Uri urlReferrer = new Uri ("http://www.microsoft.com ");
Var mockHttpContext = new Mock <HttpContextBase> ();
MockHttpContext. Setup (c => c. Request. UrlReferrer). Returns (urlReferrer );
Var mockController = this. GetMockController ();
MockController. Setup (c => c. MiddleTier. UserManager. Delete (userName). Verifiable ();
MockController. Object. ControllerContext = new ControllerContext (
MockHttpContext. Object, new RouteData (), mockController. Object );
MockController. Object. Delete (userName )...
}
In the unit test code, we Mock an HttpContextBase object so that its Request. UrlReferrer attribute returns the prepared object, constructs a new ControllerContext, and delivers it to the Controller. If our UrlReferrer can be used as a parameter for the Delete method, the unit test code will be much simpler:
[TestMethod ()]
Public void DeleteTest ()
{
String userName = "jeffz ";
Uri urlReferrer = new Uri ("http://www.microsoft.com ");
Var mockController = this. GetMockController ();
MockController. Setup (c => c. MiddleTier. UserManager. Delete (userName). Verifiable ();
MockController. Object. Delete (userName, urlReferrer )...
}
Some may ask, isn't the value taken from the UrlReferrer attribute of the Request? Why do we need to construct a ControllerContext and cannot directly set the Controller object? For example, this is much simpler:
MockController. Setup (c => c. Request. UrlReferrer). Returns (urlReferrer );
It seems feasible, but when you run it, you will find that the framework throws an exception, saying that only the interface members or override members can be Mock. Yes. The Request attribute of the Controller is not virtual and cannot be override. The Controller class is designed to restrict available paths. Imagine that if you Mock the Controller. Request attribute, but the program code is accessed through Controller. HttpContext. Request, what should I do? The method is also overloaded.
Generally, several methods are delegated to the only method, and only that method can be overwritten. In this way, the only Mock entry is determined during testing, avoiding the problem of excessive understanding of method implementation in the test code.
Return to the topic. If you want the Delete method to be connected to urlReferrer by parameters, we need to compile the components related to Model Binder:
Public class UrlReferrerModelBinder: IModelBinder
{
Public object BindModel (
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
Return controllerContext. HttpContext. Request. UrlReferrer;
}}
It can be directly applied to the parameter of Action:
Public class UrlReferrerAttribute: CustomModelBinderAttribute
{
Private static UrlReferrerModelBinder s_modelBinder =
New UrlReferrerModelBinder ();
Public override IModelBinder GetBinder ()
{
Return s_modelBinder;
}
}
Therefore, the Delete method can be written as follows:
Public ActionResult Delete (string userName, Uri urlReferrer)
{
This. MiddleTier. UserManager. Delete (userName );
Return this. Redirect (urlReferrer. ToString ());
}
Today's Code, both applications and framework class libraries, must consider "testability"