Unit Tests on views
Speaking of ASP. net mvc, we always seem to be concerned with the Controller Test-although Stephen Walther has also written about how to perform unit tests on the View from the Web Server, his method is available and unavailable. Complex construction and preparation, and judgment on the generated HTML string-is this really a unit test on The View? By carefully analyzing his code, we can find that this is actually a unit test on ViewEngine. Furthermore, if you really want to perform a unit test on ViewEngine, you should not rely on external files as they do. In my opinion, his practice is nothing ...... It seems beautiful, and it seems to win some "applause", but does the applause come from his solution or everyone's impulse?
If you want to perform unit tests on the view, you still need to render the content in the browser. When performing unit tests on a webpage, we usually use tools such as WatiN to operate the browser, open the page, and assert its DOM element structure and content. But ...... Is this a unit test? Unfortunately, this is only one regression test or user acceptance test. Because every part of the application is busy from the presentation layer to the business logic and data access when we open a page. Unit testing focuses on "separation", separation of all concerns, and separation of all dependencies. Because of separation, we can accurately locate errors; because of separation, we can use the data we have prepared in the test.
To separate them, we must follow certain rules of use. In ASP. net mvc best practices for unit testing, I mentioned that ViewData can only be used in views, rather than relying on other content (including HttpContext ). In this way, we can construct ViewData and inject it into a view object. In fact, this Convention is destroyed in the Project template that comes with ASP. net mvc. See Views \ Shared \ LogOnUserControl. ascx, where this. User is used to view the login status of the current User. This is an attribute defined on a traditional Page Object and can be obtained directly from the current HttpContext. If this method is used, it is difficult for us to "simulate" the login status of the current user during unit testing, and thus it is difficult for the test to overwrite the various situations of the test.
Lightweight Test Automation Framework
Here, Lao Zhao recommends using the Lightweight Test Automation Framework (LTAF) provided by ASP. NET Team as a Test tool. It has been updated to Feb Update on CodePlex. Similar to WatiN and Selenium, this framework allows a browser to write regression tests for applications. Although in some aspects (such as DOM element selection), LTAF has its own uniqueness:
Because it runs directly in a browser, it naturally supports existing-and any browser that may appear in the future.
Because it is directly deployed on the tested website, the test code and website page are in the same process.
The first advantage is not worth mentioning, and the second is the key. Imagine that WatiN and Selenium both open the page by writing code in the browser. This means that our testing code and the tested webpage are in different processes. Under this premise, if we want to pass the data defined in the test code to the tested webpage (that is, the view object), we must conduct cross-process communication. No matter how it is implemented, it cannot escape the "serialization" path, which undoubtedly increases the complexity. After LTAF is used, this problem instantly disappears, because we can "pass" the test data directly in the memory, and everything is just a reference.
However, everything has two sides, and LTAF also has some shortcomings that are hard to come by nature and can never be remedied. For example:
Because LTAF places the page to be tested in a Frame, the properties of window. top and other browser-based frame structures on the page will be changed.
Because the essence of LTAF is to use JavaScript to operate the DOM, this means that any operation that will block the program (such as alert) cannot be used, otherwise the entire test process will be blocked.
Fortunately, neither of these two points is a serious problem. For the first method, you only need to write a custom getTop method to replace the direct access to windows. top. In the second case, Lao Zhao never liked alert or confirm's "Pure browser functions" because they bring poor user experience, what's more, the current JavaScript class library/framework can easily make this effect, what do you think?
For details about how to use LTAF, refer to its Release Note. It is strange that Lao Zhao finds that using LTAF directly in the project has some minor problems (but why is it all normal in its example ?), Therefore, some minor modifications were made. Note ~ Some JavaScript code at the end of the \ UnitView \ DriverPage. aspx file.
UnitView usage
So Lao Zhao compiled a UnitView component to help us construct the data required for a unit test. With the data, the view can be directly presented in the browser. For example:
[WebTestClass]
Public class HomeTests
{
[WebTestMethod]
Public void LoggedOnIndexTest ()
{
Var data = new TestViewData
{
ControllerName = "Home ",
ActionName = "Index ",
Model = new IndexModel
{
Message = "Welcome guys! ",
Identity = new UserIdentity
{
IsAuthenticated = true,
Name = "Jeffrey Zhao"
}
}
};
HtmlPage page = new HtmlPage (TestViewData. GenerateHostUrl (data ));
// Assert title
Assert. AreEqual ("Home Page", page. Elements. Find ("title", 0). GetInnerText ());
// Assert head element
Var mainContent = page. Elements. Find ("main ");
Var head2 = mainContent. ChildElements. FindAll ("h2"). Single ();
Assert. AreEqual (data. Model. Message, head2.GetInnerText (), "Message shocould be displayed .");
Var loginTabInnerText = page. Elements. Find ("logindisplay"). GetInnerTextRecursively ();
Assert. IsTrue (loginTabInnerText. Contains ("Welcome"), "'Welcome 'missed .");
Assert. IsTrue (loginTabInnerText. Contains (data. Model. Identity. Name), "Login name missed .");
}
}
Naturally, Web Server is indispensable. Fortunately, separation allows our views to only involve the simplest test data, so that the simple Web Server provided by VS is sufficient. In the above Code, we directly constructed a strong type of TestViewData object, which contains all the data required to present a view:
Cotroller and Action names. Theoretically, entering the same view with different controllers and actions may produce different results.
View and Master name. If this parameter is omitted, the default view is used, which is determined by the Controller and Action values.
ViewData and Model.
The TestViewData. GenerateHostUrl method saves data and returns a URL. Access this URL to obtain the corresponding View content.
If you want to use UnitView, you can download the source code and example of UnitView from the above link and try it on the local machine. Note the following when using UnitView:
Direct the output path of the Tests project to the bin directory of the tested website, so that you can get the correct assembly at runtime without adding additional references to the website.
Change ~ Copy the \ UnitView directory to the root directory of your website (remove this directory when publishing the website ). If you want to use other directories, follow the following UnitView for analysis.
Edit ~ In the \ UnitView \ Web. config file, modify MvcApp. Tests. dll to your own Assembly that contains the test code.
UnitView Implementation Analysis
The UnitView component is very simple and is hardly worth mentioning. The TestViewData type contains all the data required for the test. TestViewData inherits TestViewData and provides a strongly Typed Access Method for Model attributes. They are not analyzed.
In addition, there are some static methods for TestViewData:
Public class TestViewData
{
Static TestViewData ()
{
PersistentProvider = new InProcPersistentProvider ();
}
Public static IPersistentProvider PersistentProvider {get; set ;}
Public static string GenerateHostUrl (TestViewData data)
{
Var key = PersistentProvider. Save (data );
Return ViewHostHandlerUrl + "? Key = "+ HttpUtility. UrlEncode (key );
}
Private static string ViewHostHandlerUrl
{
Get
{
Return ConfigurationManager. deleettings ["UnitView_ViewHostHandlerUrl"]
?? "/UnitView/ViewHostHandler. ashx ";
}
}
Internal static TestViewData Load (string key)
{
Return PersistentProvider. Load (key );
}
...
}
The GenerateHostUrl method delegates PersistentProvider to save the object and obtain a key. This key will be spliced on the ViewHostHandlerUrl attribute, which is the path to be tested. As you can see from the code, if you do not want to use the default test path, you only need to add a target address to the AppSettings node of web. config.
The PersistentProvider attribute is of the IPersistentProvider interface type. The Save, Load, and Remove methods are defined. IPersistentProvider has only one implementation in the project: InProcPersistentProvider, which stores TestViewData in a dictionary in the memory. This implementation is enough for UnitView to run in combination with LTAF (the same process features of LTAF play a key role ). However, if you still want to use WatiN and other independent process testing tools, you must implement your own IPersistentProvider type. For example, you can implement a FilePersistentProvider to serialize TestViewData to an external file so that it can be retrieved as appropriate.
Another critical type is UnitView. Engine. ViewHostHandler:
Public class ViewHostHandler: IHttpHandler
{
Private HttpContext Context {get; set ;}
Public void ProcessRequest (HttpContext context)
{
This. Context = context;
ControllerContext controllerContext = new ControllerContext (
New HttpContextWrapper (context ),
This. Data. RouteData,
New MockController ());
New ViewResult
{
MasterName = this. Data. MasterName,
ViewName = this. Data. ViewName,
TempData = this. Data. TempData,
ViewData = this. Data. ViewData,
}. ExecuteResult (controllerContext );
}
Private string Key
{
Get
{
String key = this. Context. Request. QueryString ["key"];
If (String. IsNullOrEmpty (key ))
{
Throw new ArgumentNullException ("key ");
}
Return key;
}
}
Private TestViewData m_data;
Private TestViewData Data
{
Get
{
If (this. m_data = null)
{
This. m_data = TestViewData. Load (this. Key );
If (this. m_data = null)
{
Throw new ArgumentNullException ("Cannot retrieve the data .");
}
}
Return this. m_data;
}
}
Public bool IsReusable {get {return false ;}}
}
First, the ProcessRequest method retrieves TestViewData, constructs a ViewResult object based on the data, and finally executes its ExecuteResult method to output the View content. Because of the ExecuteRequest method, we must construct a ControllerContext object, which means that we must provide a Controller object and HttpContext encapsulation. We can see from the code that the simplest data is used here. Because a view complies with the "Conventions", it only obtains data from ViewData, so no matter what the Controller or HttpContext is.
You may wonder why the "Convention" does not allow the view to retrieve data from the HttpContext object? A Mock HttpContext object is not that difficult (thanks to various powerful Mock frameworks. Unfortunately, the HttpContext after Mock is difficult to serialize, which almost eliminates the possibility of cross-process communication. This is undoubtedly a disaster for friends who use WatiN and Selenium for testing. Under the balance, Lao Zhao decided to give up support for HttpContext.
Note 1: currently UnitView is built based on ASP. net mvc rc. After RTM is released, I will update it as necessary. Follow this article by Lao Zhao and the Code (http://code.msdn.microsoft.com/UnitView) hosted on MSDN Code Gallery ).
NOTE 2: in ASP. net mvc best practices for unit testing, I also include the UnitView component. The implementation is slightly different-please refer to this article.