|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
This is the story of the Tortoise and the Long Hair.Both Tortoise and Long Hair were developers at the same company and were tasked with building a high profile application for the company. The application was supposed to be a sort of pyramid scheme for selling bricks that the CEO had thought up. He was quite sure it would drive the company’s profits through the roof and revolutionize the brick selling industry. Their boss, Mr. Wolf, sat them both down before the project got started to go over the details. Code quality and reliability were of utmost importance, so they would be expected to use the new Aspnet MVC Framework to enable extremely testable code. "Lastly, I just wanted to let you both know that there's a huge promotion in this for one of you at the end of this project" explained Mr. Wolf "which I fully expect to spur on a nice friendly rivalry between the two of you. Oh, and the other one of you will likely get canned." So they both got to work right away. Things were going along well. They were making sure to thoroughly test all Model and Controller code. Like any project, the changes came pouring, but thanks to an ever growing suite of unit tests the two developers weren’t too worried. But then it came time to test the Views. Neither of them could find any examples of how to write a unit test for their View code. This seemed strange, so they decided to split up for the rest of the afternoon and see what they each could come up with. The Tortoise started with the obvious and wrote some code similar to the following MyView view = new MyView(); HtmlTextWriter writer = new HtmlTextWriter(); view.Render(writer); The Tortoise sat back and looked at his masterpiece. He was clearly a superior coder. He set about writing up his findings for Mr. Wolf, but just before he clicked the "submit" button he thought to himself, "Maybe I should actually try running this code… just for good measure". Needless to say, he was sadly disappointed. It turned out that what he got back didn’t include any of the html from the aspx files. After further research he learned that, at runtime, the Aspnet framework dynamically created a new class that inherits from the code behind file but also includes all the html from the aspx file. That dynamic class is what actually handles the html rendering. The Long Hair on the other hand, already knew something of this runtime compilation and started with a bit more sophisticated solution. He decided to find the code Aspnet uses to dynamically compile the page and use that code to render the view. The Long Hair spent the next few hours, googling, looking at documentation, and decompiling code in the System.Web namespaces. Suddenly there it was, StringWriter output = new StringWriter(); SimpleWorkerRequest request = new SimpleWorkerRequest("MyView.aspx", "", output); HttpRuntime.ProcessRequest(request); Sadly, the Long Hairs code failed too. He decompiled the source where the error occurred and took a look around. No problem. It was simply a configuration issue. He found the needed configuration, altered his code to set it to the required value, and tested again. Failure. No problem. Check the decompiled source. Hmm, another configuration issue… After repeating the above process some 30 times, the Long Hair was starting to get the feeling that this wasn’t a great solution. For one, his code was getting ugly. There were all sorts of crazy workarounds and hacks to set the needed configurations. Secondly, with all the configurations that needed to be made, who knows what state the application would be in by the time the test finished running. He started to wonder if getting his View test to pass wouldn’t end up having unexpected side effects on the rest of the code. The next day, the two developers came together with their boss and shared their findings. Mr. Wolf was furious. Why were they wasting there time goofing around instead of writing "real code", he demanded? After the meeting, both Tortoise and Long Hair were very discouraged. They cried on each others shoulder for a few minutes and then got back to work. Tortoise went online and found a few existing tools for testing web UI’s. Some of them launch a browser and tested through its DOM, others sent an actual web request to a web server and parsed the response. All of them seemed promising, so Tortoise downloaded one and started working. Although he was able to quickly get a test up and running it was really more of an integration test than a unit test. There was no way to test just the View without also running the Controller, the Model, and the Database. Still, it was something. Best of all, it ran at Tortoises favorite speed… veeeeeeeeeery sloooooooooooow. The Long Hair on the other hand felt sure he could get his original idea working. After some more sifting, he stumbled upon This was exactly what was needed. The last argument is a string called The second argument is a string called The last argument (well, actually the first argument) was a little trickier. It takes a public class CrossDomainProxy : MarshalByRefObject { public string ProcessRequest() { StringWriter output = new StringWriter(); SimpleWorkerRequest request = new SimpleWorkerRequest("MyView.aspx", "", output); HttpRuntime.ProcessRequest(request); return output.GetStringBuilder().ToString } } and then passed it to the CreateApplicationHost method CrossDomainProxy proxy = (CrossDomainProxy)ApplicationHost.CreateApplicationHost(
typeof(CrossDomainProxy),
"/",
"C:\Inetpub\wwwroot\MVCTestWebApp");
Again, failure. For some reason it couldn’t find the assembly containing his Then all at once it hit him. The currently executing assembly was sitting in his current project folder, but the new Aspnet AppDomain was executing at "C:\Inetpub\wwwroot\MVCTestWebApp". It was looking for the He was close now. This would work well for a traditional Aspnet page, but it was still missing a critical piece for MVC Views. He still needed a way to set the Both Tortoise and Long Hair arrived at work the next day at the usual time, 10:00am, and walked into their office to find Mr. Wolf and a few other important looking men waiting for them. Apparently, Mr. Wolf had promised a demo and the two were expected to show their progress. The Long Hair sheepishly explained that he didn’t have anything he could show yet. At this, Mr. Wolf gave him a disapproving scowl as the important looking men whispered among themselves. The Tortoise on the other hand, sensing the opportunity, quickly set about showing his work. However, his excitement quickly turned to horror as he immediately entered "demo hell". Nothing worked. All the pages that were working the day before were crashing today. After the important looking men left and Mr. Wolf finished his 20 minute rant, the two developers regrouped. "What happened?" asked the Long Hair. "last night you said you had written tests for your UI?" The Tortoise frowned. "I have tests" he said. "The problem is they take so long to run that I don’t run them every time a make a change." So, while the Tortoise set about fixing his broken views, the Long Hair picked up where he left off. After a few more hours spent looking through the MVC class libraries, he updated his public static string RenderView(string controllerName, string viewName, string queryString, object viewData) { StringBuilder result = new StringBuilder(); string virtualPath = string.Format("/{0}/{1}", controllerName, viewName); IHttpContext context = CreateHttpContext(result, virtualPath, queryString); ControllerContext controllerContext = CreateControllerContext(context, controllerName); ViewContext viewContext = new ViewContext(controllerContext, viewData, new TempDataDictionary(context)); CreateView(viewName, viewData, controllerContext).RenderView(viewContext); context.Response.Flush(); return result.ToString(); } private static IHttpContext CreateHttpContext(StringBuilder result, string virtualPath, string queryString) { SimpleWorkerRequest wr = new SimpleWorkerRequest(virtualPath, queryString, new StringWriter(result)); HttpContext.Current = new HttpContext(wr); return new HttpContextWrapper(HttpContext.Current); } private static ControllerContext CreateControllerContext(IHttpContext context, string controllerName) { RouteData routeDate = new RouteData(); routeDate.Values.Add("controller", controllerName); return new ControllerContext(context, routeDate, new Controller()); } private static IView CreateView(string viewName, object viewData, ControllerContext controllerContext) { IViewFactory viewFactory = new WebFormViewFactory(); return viewFactory.CreateView(controllerContext, viewName, null, viewData); } The project already had a Person person = new Person() { Name="Eric", Age=31 } proxy.RenderView("Test", "MyView", "", person); It failed again. On further investigation, it was yet another issue with cross AppDomain communication. He was trying to pass a reference to a After a fair amount of fiddling and a bit of black magic, he came up with a solution. First he created the following delegate public delegate object CreateViewData(); Then he created the following class to simplify interaction with the proxy public class AspnetHost { private CrossDomainProxy proxy; public AspnetHost(string webDirectory) { proxy = (CrossDomainProxy)ApplicationHost.CreateApplicationHost(typeof(CrossDomainProxy), "/", Path.GetFullPath(webDirectory)); } public string RenderView(CreateViewData createViewDataDelegate, string controllerName, string viewName, string queryString) { Type type = createViewDataDelegate.Method.DeclaringType; string methodName = createViewDataDelegate.Method.Name; return proxy.RenderView(type.Assembly.Location, type.FullName, methodName, controllerName, viewName, queryString); } } Lastly, he added the following method to his internal string RenderView(string sourceAssemblyPath, string typeName, string createViewDataMethodName, string controllerName, string viewName, string queryString) { Assembly assembly = Assembly.LoadFile(sourceAssemblyPath); Type type = assembly.GetType(typeName); MethodInfo methodInfo = type.GetMethod(createViewDataMethodName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo == null) { throw new ApplicationException("The provided delegate must point to a static method."); } object viewData = methodInfo.Invoke(null, null); return RenderView(controllerName, viewName, queryString, viewData); } There’s some crazy black magic here, so let me explain what’s going on. The With everything in place, the Long Hair was now able to write the following test [TestFixture]
public class PersonViewTests
{
[Test]
public void LoadEditView()
{
AspnetHost host = new AspnetHost("../../../WebApp");
string result = host.RenderView(CreateViewData, "People", "Edit", "");
Assert.IsTrue(result.Contains("Jack"));
Assert.IsTrue(result.Contains("49"));
}
public static object CreateViewData()
{
return new Person() { Name = "Jack", Age = 49 };
}
}
At long last, success! They would now be able to unit test their views. More importantly the tests ran quite fast, so they could be run anytime a change was made. This insured that any code change that caused a view to break was immediately found and fixed. Things were moving again. The project was being developed at a remarkable speed, but the code quality remained very high. Best of all, demos were going off without a hitch. A few weeks later Mr. Wolf came into their office. The company’s legal team had just informed the CEO that pyramid schemes were illegal, so the project was being scrapped. "What about the big promotion?" asked the Long Hair. "What big promotion?" responded Mr. Wolf. "I have no idea what you’re talking about." And he walked out of the office. NOTE: For more on the history of the Mr. Wolf, click here.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||