ASP.NET MVC Session at Mix08, TDD and MvcMockHelpers
NOTE: This post is based on an older preview version of ASP.NET MVC and details have very likely CHANGED. Go to http://www.asp.net/mvc or http://www.asp.net/forums for updated details and the latest version of ASP.NET MVC.
All the sessions from Mix are up on http://sessions.visitmix.com/ for your viewing pleasure. I had a total blast giving the ASP.NET MVC Talk. The energy was really good and the crowd (of around 600, I hear) was really cool.
You can download the MVC talk in these formats:
- ASP.NET MVC Preview 2 - Mix 08 - Silverlight
- ASP.NET MVC Preview 2 - Mix 08 - WMV
- ASP.NET MVC Preview 2 - Mix 08 - iPod
- ASP.NET MVC Preview 2 - Mix 08 - Zune
- ASP.NET MVC Preview 2 - PowerPoint (PPTX)
- Comment on the ASP.NET MVC Preview 2 Talk
- UPDATED: ASP.NET MVC Preview 2 Cheesy Northwind Sample Code
I think the sound is a little quiet, so I had to turn it up some. It's better turned up a bit so you can hear the interaction with the crowd.
Here's some of the code from the talk you might be interested in. I'll post the rest very soon.
MvcMockHelpers
The first are the MVCMockHelpers used in the Test Driven Development part of the talk, and also in the 4th ASP.NET MVC Testing Video up at www.asp.net/mvc.
NOTE AND DISCLAIMER: This is just a little chunks of helper methods, and I happened to use Rhino Mocks, an Open Source Mocking Framework, the talk at Mix. At my last company I introduced TypeMock and we bought it and lately I've been digging on Moq also. I'm not qualified yet to have a dogmatic opinion about which one is better, because they all do similar things. Use the one that makes you happy. I hope to see folks (that's YOU Dear Reader) repost and rewrite these helpers (and better, more complete ones) using all the different mocking tools. Don't consider this to be any kind of "stamp of approval" for one mocking framework over another. Cool?
Anyway, here's the mocking stuff I used in the demo. This is similar to the stuff PhilHa did last year but slightly more complete. Still, this is just the beginning. We'll be hopefully releasing ASP.NET MVC bits on CodePlex maybe monthly. The goal is to release early and often. Eilon and the team have a lot more planned around testing, so remember, this is Preview 2 not Preview 18.
MvcMockHelpers - RhinoMocks
using System; using System.Web; using Rhino.Mocks; using System.Text.RegularExpressions; using System.IO; using System.Collections.Specialized; using System.Web.Mvc; using System.Web.Routing; namespace UnitTests { public static class MvcMockHelpers { public static HttpContextBase FakeHttpContext(this MockRepository mocks) { HttpContextBase context = mocks.PartialMock<httpcontextbase>(); HttpRequestBase request = mocks.PartialMock<httprequestbase>(); HttpResponseBase response = mocks.PartialMock<httpresponsebase>(); HttpSessionStateBase session = mocks.PartialMock<httpsessionstatebase>(); HttpServerUtilityBase server = mocks.PartialMock<httpserverutilitybase>(); SetupResult.For(context.Request).Return(request); SetupResult.For(context.Response).Return(response); SetupResult.For(context.Session).Return(session); SetupResult.For(context.Server).Return(server); mocks.Replay(context); return context; } public static HttpContextBase FakeHttpContext(this MockRepository mocks, string url) { HttpContextBase context = FakeHttpContext(mocks); context.Request.SetupRequestUrl(url); return context; } public static void SetFakeControllerContext(this MockRepository mocks, Controller controller) { var httpContext = mocks.FakeHttpContext(); ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller); controller.ControllerContext = context; } static string GetUrlFileName(string url) { if (url.Contains("?")) return url.Substring(0, url.IndexOf("?")); else return url; } static NameValueCollection GetQueryStringParameters(string url) { if (url.Contains("?")) { NameValueCollection parameters = new NameValueCollection(); string[] parts = url.Split("?".ToCharArray()); string[] keys = parts[1].Split("&".ToCharArray()); foreach (string key in keys) { string[] part = key.Split("=".ToCharArray()); parameters.Add(part[0], part[1]); } return parameters; } else { return null; } } public static void SetHttpMethodResult(this HttpRequestBase request, string httpMethod) { SetupResult.For(request.HttpMethod).Return(httpMethod); } public static void SetupRequestUrl(this HttpRequestBase request, string url) { if (url == null) throw new ArgumentNullException("url"); if (!url.StartsWith("~/")) throw new ArgumentException("Sorry, we expect a virtual url starting with \"~/\"."); SetupResult.For(request.QueryString).Return(GetQueryStringParameters(url)); SetupResult.For(request.AppRelativeCurrentExecutionFilePath).Return(GetUrlFileName(url)); SetupResult.For(request.PathInfo).Return(string.Empty); } } }
MvcMockHelpers - Moq
Here's the same thing in Moq. Muchas gracias, Kzu.
using System; using System.Web; using System.Text.RegularExpressions; using System.IO; using System.Collections.Specialized; using System.Web.Mvc; using System.Web.Routing; using Moq; namespace UnitTests { public static class MvcMockHelpers { public static HttpContextBase FakeHttpContext() { var context = new Mock<httpcontextbase>(); var request = new Mock<httprequestbase>(); var response = new Mock<httpresponsebase>(); var session = new Mock<httpsessionstatebase>(); var server = new Mock<httpserverutilitybase>(); context.Expect(ctx => ctx.Request).Returns(request.Object); context.Expect(ctx => ctx.Response).Returns(response.Object); context.Expect(ctx => ctx.Session).Returns(session.Object); context.Expect(ctx => ctx.Server).Returns(server.Object); return context.Object; } public static HttpContextBase FakeHttpContext(string url) { HttpContextBase context = FakeHttpContext(); context.Request.SetupRequestUrl(url); return context; } public static void SetFakeControllerContext(this Controller controller) { var httpContext = FakeHttpContext(); ControllerContext context = new ControllerContext(new RequestContext(httpContext, new RouteData()), controller); controller.ControllerContext = context; } static string GetUrlFileName(string url) { if (url.Contains("?")) return url.Substring(0, url.IndexOf("?")); else return url; } static NameValueCollection GetQueryStringParameters(string url) { if (url.Contains("?")) { NameValueCollection parameters = new NameValueCollection(); string[] parts = url.Split("?".ToCharArray()); string[] keys = parts[1].Split("&".ToCharArray()); foreach (string key in keys) { string[] part = key.Split("=".ToCharArray()); parameters.Add(part[0], part[1]); } return parameters; } else { return null; } } public static void SetHttpMethodResult(this HttpRequestBase request, string httpMethod) { Mock.Get(request) .Expect(req => req.HttpMethod) .Returns(httpMethod); } public static void SetupRequestUrl(this HttpRequestBase request, string url) { if (url == null) throw new ArgumentNullException("url"); if (!url.StartsWith("~/")) throw new ArgumentException("Sorry, we expect a virtual url starting with \"~/\"."); var mock = Mock.Get(request); mock.Expect(req => req.QueryString) .Returns(GetQueryStringParameters(url)); mock.Expect(req => req.AppRelativeCurrentExecutionFilePath) .Returns(GetUrlFileName(url)); mock.Expect(req => req.PathInfo) .Returns(string.Empty); } } }
Maybe RoyO will do the same thing in TypeMock in the next few hours and I'll copy/paste it here. ;)
MvcMockHelpers - TypeMock
Thanks to Roy at TypeMock.
using System; using System.Collections.Specialized; using System.Web; using System.Web.Mvc; using System.Web.Routing; using TypeMock; namespace Typemock.Mvc { static class MvcMockHelpers { public static void SetFakeContextOn(Controller controller) { HttpContextBase context = MvcMockHelpers.FakeHttpContext(); controller.ControllerContext = new ControllerContext(new RequestContext(context, new RouteData()), controller); } public static void SetHttpMethodResult(this HttpRequestBase request, string httpMethod) { using (var r = new RecordExpectations()) { r.ExpectAndReturn(request.HttpMethod, httpMethod); } } public static void SetupRequestUrl(this HttpRequestBase request, string url) { if (url == null) throw new ArgumentNullException("url"); if (!url.StartsWith("~/")) throw new ArgumentException("Sorry, we expect a virtual url starting with \"~/\"."); var parameters = GetQueryStringParameters(url); var fileName = GetUrlFileName(url); using (var r = new RecordExpectations()) { r.ExpectAndReturn(request.QueryString, parameters); r.ExpectAndReturn(request.AppRelativeCurrentExecutionFilePath, fileName); r.ExpectAndReturn(request.PathInfo, string.Empty); } } static string GetUrlFileName(string url) { if (url.Contains("?")) return url.Substring(0, url.IndexOf("?")); else return url; } static NameValueCollection GetQueryStringParameters(string url) { if (url.Contains("?")) { NameValueCollection parameters = new NameValueCollection(); string[] parts = url.Split("?".ToCharArray()); string[] keys = parts[1].Split("&".ToCharArray()); foreach (string key in keys) { string[] part = key.Split("=".ToCharArray()); parameters.Add(part[0], part[1]); } return parameters; } else { return null; } } public static HttpContextBase FakeHttpContext(string url) { HttpContextBase context = FakeHttpContext(); context.Request.SetupRequestUrl(url); return context; } public static HttpContextBase FakeHttpContext() { HttpContextBase context = MockManager.MockObject<HttpContextBase>().Object; HttpRequestBase request = MockManager.MockObject<HttpRequestBase>().Object; HttpResponseBase response = MockManager.MockObject<HttpResponseBase>().Object; HttpSessionStateBase sessionState = MockManager.MockObject<HttpSessionStateBase>().Object; HttpServerUtilityBase serverUtility = MockManager.MockObject<HttpServerUtilityBase>().Object; using (var r = new RecordExpectations()) { r.DefaultBehavior.RepeatAlways(); r.ExpectAndReturn(context.Response, response); r.ExpectAndReturn(context.Request, request); r.ExpectAndReturn(context.Session, sessionState); r.ExpectAndReturn(context.Server, serverUtility); } return context; } } }
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Any chance of Blogging about Asp.Net MVC with any one of dynamic languages in near future.
Its high time, since there are no discussions around. It seems Dynamic languages were totally neglected in this MIX 2008, and SilverLight took away all the attraction.
Since the DLR bits are around... may be you can start this now.
Thanks
IronRuby
In Ruby on Rails I can let the action respond to formats (like html (http request), js (ajax), xml and so on) and then render the view for the requested format.
Can I do the same thing i ASP.NET MVC today, if not, come this funcionality be added later?
Thanks
Johan
:P
Link: Typemock MVC Mock Helpers
Roy.
Just watched your MIX presentation. I have to say that it was one of the most entertaining I've seen in a long time.
It was great to see you referencing Linux, alt.net and some of the open source tools out there, the push on TDD and CI... and the jokes on ScottGu, too. xD
It was a great presentation. There's one thing I'm still unclear on, though. Is MVC replacing Web Forms?
NO, absolutely not. It's just ANOTHER OPTION to build ASP.NET apps, another web project for you to choose from. WebForms will continue to be fully supported. I think it was clear from the presentation.
btw I like your humour Scott, way to do it. Everyone could see you're foreigner, not from the Microsoft ;), oh wait, already 6 months there ;P
The MVC session at Mix was fantastic. You crammed in the MVC pattern, the new MVC framework, TDD, mocking, and an entertaining delivery that was unmatched in any other session! Keep doing what you are doing!
I also wanted to thank you for your graciousness in the scavenger hunt on Tuesday evening. You can read my run-down and see the photo we took on my blog post - Mix08 - Mixing it up with a Scavenger Hunt
Thanks again!
I have developed my research site by MVC framework since the first preview released. I got the problem when I use site master page. It was discussed by other guys in ASP.NET forum. URL: http://forums.asp.net/t/1195178.aspx
It seems the issue still not be resolved at this new preview 2. I can only use the hacked solution in this thread. Could you take a look the issue?
You should give masterclasses in presentation!
I took time to watch the presentation this morning. I found it very interesting.
You guys seem to be taking it all very seriously, and I think you are doing a great job so far. Some of the points I have personally raised were addressed in the presentation - thanks.
I think the greatest risk to getting developers to take this on will be what happens once you release version 1.0. Because most of us are inherently lazy, uptake will be slow in enterprise development - it is a lot of new technology to learn. Most MS enterprise developers I know don't even know HTML, which is why they use WebForms in the first place. (It's easy to learn if they know WinForms.) And don't even ask about CSS. NHaml is great too, but it will go over most people's heads.
It seems like your target market are the web developers of the world - just like the audience for Ruby on Rails. Unfortunately it is going to be a long time before IIS7 (necessary for Routing to work properly, I assume) and SQL Server are as mature and cheap to run as Rails and MySQL. (And before you say "SQL Express, dude!", my hoster - webhost4life - just turned SQL Express off because of its poor performance.) If all that means that uptake of ASP.Net MVC is slow, I hope it means you don't give up.
I think your decision to make Routes available to all web apps will be great for me. I will build it into my own apps. Can you use the Html.Action link thing from within WebForms too?
But overall, I really like it. I'm looking forward to seeing the result - and then version 2. :-)
Best regards,
Richard
Last year I was excited about the ASP.Net Futures, and Silverlight, because of the DLR. MIX07 was full of dynamic language stuff, and as far as I remember, IronRuby was one of your pet topics. Since then it's gone AWOL, and there is absolutely nothing appearing about it. I personally liked IronPython, but there's no support for it now - i.e. no tutorials, no help, no ASP.Net Futures, etc, etc.
What's happened to it?
Richard
Thanks,
Rick
The session was very nice and kept me glued all throughout the session. I think having a humourous tone in the talk was really nice. Might not be possible, but such tone should be used in screencasts too! that way learning is more fun!
The sessions from MIX08 are absolutely great!
A very interesting concept, in my opinion, is the combination of ASP.NET MVC and Silverlight 2.0. Can these two be hooked up together? Is it something you guys are thinking of, and is this something that you recommend?
If so, it'd be great to maybe see a screencast on the subject, or a blog post that explains the basic concepts of working with MVC and Silverlight.
Thanks!
Will we ever see ASP.NET MVC on .NET 2, or should I rather go with Spring.NET if I'm looking for an MVC framework for .NET 2.
Thanks
Horea
I can't work something out. Using your classes + Rhino how do I tell Rhino to expect a redirect? When I execute
Controller.Response.Redirect("/Home/Index");
during record mode I get an error that the method is not implemented!
Thanks
Pete
You might want to use the "RedirectToAction()" method instead.
In Scott's session he asked us to blog and post about our experiences, so I have begun a post at my blog to record my Wish List. So far the "Wish List" is a single short item: I would like to be able to use the UrlHelper Action method in a Static context. In my example, I tried to use it in my MasterPage definition but could not because I did not have a ViewPage instance.
I'll add to my post above as I come across more items, otherwise, I'm just loving it! I'm sure I'll spend the rest of the month happily ensconced in MVC.
Thanks,
Joel Cochran
To be quite honest I am finding anything other than the most basic test to be a *real* nightmare, it is putting me off even continuing to write tests for the website I am working on.
My requirement is simple
01: Controller.Action(some parameters)
02: Ensure that ViewData["Message"] = "Some message"
03: Response.Redirect was executed to /Home/Index
I wouldn't have thought this would have been difficult, and I don't think I am stupid either!
Pete
In fact, none of these during recording works
MockHttpContext.Response.Redirect("/Home/Index", false);
MockHttpContext.Response.Redirect("Home/Index", false);
MockHttpContext.Response.Redirect("/Home/Index", true);
MockHttpContext.Response.Redirect("Home/Index", true);
MockHttpContext.Response.Redirect("/Home/Index");
MockHttpContext.Response.Redirect("Home/Index");
It's silly!
Just some small notes about style for the querystring parsing. First off, you can use the System.Uri class to help a bit (it'll give you in a more strongly-typed way whether there are querystring parameters), like so:
string url = "http://www.mydomain.com/somefolder/getStuff.aspx?id=1&var2=abc&var3=55";
Uri uri = new Uri(url);
then uri.Query will be "?id=1&var2=abc&var3=55", so you could do a string.IsNullOrEmpty on it.
then, you don't need to use the 'ToCharArray' method on the string, instead just pass in a char type directly, using single quotes instead of double, as follows:
string[] part = key.Split('=');
these will make the code a tiny bit easier to read, as follows:
string url = "http://www.mydomain.com/somefolder/getStuff.aspx?id=1&var2=abc&var3=55";
Uri uri = new Uri(url);
NameValueCollection parameters = new NameValueCollection();
if (!string.IsNullOrEmpty(uri.Query))
{
string[] keys = uri.Query.Substring(1).Split('&');
foreach (string key in keys)
{
string[] part = key.Split('=');
parameters.Add(part[0], part[1]);
}
}
finally, you can add all this as an extension method on the Uri class itself, as follows:
public static class UriExtensions
{
public static NameValueCollection GetQueryStringCollection(this Uri uri)
{
NameValueCollection parameters = new NameValueCollection();
if (!string.IsNullOrEmpty(uri.Query))
{
string[] keys = uri.Query.Substring(1).Split('&');
foreach (string key in keys)
{
string[] part = key.Split('=');
parameters.Add(part[0], part[1]);
}
}
return parameters;
}
}
Two comments on this: I made the method return an empty collection instead of null, just made more sense to me at the time. Also, the Substring(1) is to get rid of the initial '&' character.
Finally, the usage, which appears as follows:
string url = "http://www.mydomain.com/somefolder/getStuff.aspx?id=1&var2=abc&var3=55";
Uri uri = new Uri(url);
NameValueCollection parameters = uri.GetQueryStringCollection();
I think we need an event or partial method that will execute inside the controller prior to the requested Action beginning. It would be very handy to execute Controller wide behavior at this level without needing every Action method to explicitly call another method. I imagine this could be used for logging, validation, security, and many other things.
Joel
Cheers
Thanks for the nice and very very helpful videos. Just wanted to ask , you , i culd have any .net samples on MVC that you showed in the videos.
Regards
Add an HtmlHelper method for a simple TableRow: this would be sure to assign the property names automatically to facilitate the use of UpdateFrom().
So far, I really like what I see. I'm trying to work my way through the rest of the videos and examples I can find. I have come across something, and I don't know if it is related to the project or not. I tried to use the Url.ActionLink() method inside a form action attribute and IntelliSense would not work. If I move the code out side the form to an independent code block it works just fine (confirming my references are OK). I tried it both with and without double quotes and no joy either way. It just seems strange to me.
Anyway, thanks for everything.
Joel
I've been going through the Advanced video, trying to figure out how to implement a site-wide error handler. This should be better than relying on the webserver or developing a custom 404 page. I've watched the section on InterceptController several times, which I think along with a *catchall Route could help me come up with a solution, but I have one burning question: how are HomeController and InterceptController related? I can see where InterceptController is handling whatever Controller is passed in, but I never can see how HomeController is made aware of InterceptController.
How does this work? Did I miss something or is this more magic?
Thanks,
Joel
Add an HtmlHelper method for a simple TableRow: this would be sure to assign the property names automatically to facilitate the use of UpdateFrom().Personally, I disagree with this approach, Joel. One of the big upsides to MVC, in my eyes, is that discrete control over the HTML is fairly broadly maintained without having to hack WYSIWYG code. It's a slippery-slope to create tags that are partial to the rest of the structure (i.e., must be within a table), because the next step is to just design the table... and, well, that sort of thinking is what makes such code bloat in the WebForms model.
My 2 cents,
-Matt
I also have to say I like Moq best out of the other testing frameworks.
Thanks and get us more videos :)
-Emad
using (mocks.Playback())
{
var result = controller.About() as RenderViewResult;
Assert.AreEqual("About", result.ViewName);
}
instead of
using (mocks.Playback())
{
controller.About();
Assert.AreEqual("About", fakeViewEngine.ViewContext.ViewName);
}
the fake view engine does not need to be changed
This is not an appropriate place to place my query, but need help,
how can i add htmlAttributes like form id in
Html.Form("CandidateAdd", "Candidate", FormMethod.Post, htmlAttributes) in mvc preview 2 previously it was like this Html.Form("CandidateAdd", "Candidate", new{id="frmcandidate"})
Regards,
Taufiq Yusuf
<%Dictionary<string, object> HtmlAttributes = new Dictionary<string, object>(); %>
<% HtmlAttributes["id"] = "frm"; %>
<% using (Html.Form("CandidateAdd", "Candidate", FormMethod.Post, HtmlAttributes))
Comments are closed.
thanks for pasting the Moq version :)
The escaping of the generics is kind of messed up (again). =
i.e.
var context = new Mock();
var request = new Mock();
var response = new Mock();
var session = new Mock();
var server = new Mock();
is missing all the <T> on all mocks. :(. Do you use VS Paste (http://gallery.live.com/liveItemDetail.aspx?li=d8835a5e-28da-4242-82eb-e1a006b083b9&l=8) from WLW? I've never again had a problem with pasting code since I started using it...