Previously, I wrote about how to mock a ViewEngine for one of my unit tests. Today, I came across a question on StackOverflow asking about how to unit test the out put of DataAnnotation attribute DisplayFormat. I'm very interested in Unit Test so I decide to find the answer, another reason is to increase my reputation on StackOverflow :D. Honestly, I don't exactly know the reason why to test the output because for me, we don't need to test code that was not written by us. Anyways, this is a interesting question for me. Let's say we have a ViewModel using DisplayFormat like below:
public class UserViewModel { [DisplayFormat(DataFormatString = "{0:dd/MM/yy}")] public DateTime Birthday { get; set; } }And here is the basic unit test to verify the output
[Test] public void Test_output_display_format_for_Birthday_property() { // Arrange var _model = new UserViewModel {Birthday = DateTime.Parse("28/07/11") }; var helper = MvcTestControllerBuilder.GetHtmlHelper<UserViewModel>(); helper.ViewData.Model = _model; // Action var result = helper.DisplayFor(x => x.Birthday); // Assert Assert.That(result.ToHtmlString(), Is.EqualTo("28/07/11")); }Apparently, this test failed like the description in the StackOverflow question. I decided to debug the MVC source code and found 2 reasons: + First, the MVC framework will find the ActionCacheItem using method GetActionCache:
internal static Dictionary<string, ActionCacheItem> GetActionCache(HtmlHelper html) { HttpContextBase httpContext = html.ViewContext.HttpContext; if (!httpContext.Items.Contains(cacheItemId)) { Dictionary<string, ActionCacheItem> dictionary = new Dictionary<string, ActionCacheItem>(); httpContext.Items[cacheItemId] = dictionary; return dictionary; } return (Dictionary<string, ActionCacheItem>) httpContext.Items[cacheItemId]; }It'll try to find the cache item in httpContext.Items but the Items is null. So the first thing we need to mock the value for httpContext.Items:
var helper = MvcTestControllerBuilder.GetHtmlHelper<UserViewModel>(); helper.ViewContext.HttpContext.Setup(x => x.Items).Returns(new Dictionary<string, object>());+ Secondly, the MVC framework will try to find the display template for "DateTime" and "String". Obviously we don't have those stuff in the Unit test environment. The code is located in TemplateHelpers.cs:
internal static string ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions) { Dictionary<string, ActionCacheItem> actionCache = GetActionCache(html); Dictionary<string, Func<HtmlHelper, string>> dictionary2 = getDefaultActions(mode); string str = modeViewPaths[mode]; foreach (string str2 in getViewNames(viewData.ModelMetadata, new string[] { templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName })) { ActionCacheItem item; Func<HtmlHelper, string> func; string key = str + "/" + str2; if (actionCache.TryGetValue(key, out item)) { if (item != null) { return item.Execute(html, viewData); } continue; } ViewEngineResult result = ViewEngines.Engines.FindPartialView(html.ViewContext, key); if (result.View != null) { ActionCacheViewItem item2 = new ActionCacheViewItem { ViewName = key }; actionCache[key] = item2; using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) { result.View.Render(new ViewContext(html.ViewContext, result.View, viewData, html.ViewContext.TempData, writer), writer); return writer.ToString(); } } if (dictionary2.TryGetValue(str2, out func)) { ActionCacheCodeItem item3 = new ActionCacheCodeItem { Action = func }; actionCache[key] = item3; return func(MakeHtmlHelper(html, viewData)); } actionCache[key] = null; } throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, MvcResources.TemplateHelpers_NoTemplate, new object[] { viewData.ModelMetadata.RealModelType.FullName })); }So the main thing we need to do is somehow mock the result at line "19" to make the view engine return a ViewEngineResult that has View property equals to null. I make a helper method similar to this post to do this:
public static ViewEngineResult SetupNullViewFor(string viewName) { Mock<IViewEngine> mockedViewEngine = GetCurrentMockViewEngine() ?? new Mock<IViewEngine>(); var viewEngineResult = new ViewEngineResult(new List<string>()); mockedViewEngine.Setup(x => x.FindPartialView(It.IsAny<ControllerContext>(), viewName, It.IsAny<bool>())) .Returns(viewEngineResult); mockedViewEngine.Setup(x => x.FindView(It.IsAny<ControllerContext>(), viewName, It.IsAny<string>(), It.IsAny<bool>())) .Returns(viewEngineResult); ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(mockedViewEngine.Object); return viewEngineResult; } private static Mock<IViewEngine> GetCurrentMockViewEngine() { foreach(var v in ViewEngines.Engines) { try { return Mock.Get<IViewEngine>(v); } catch (Exception) { } } return null; }With all of these stuff, the final unit test would look like:
[Test] public void Test_output_display_format_for_Birthday_property() { // Arrange MvcTestFixtureHelper.SetupNullViewFor("DisplayTemplates/DateTime"); MvcTestFixtureHelper.SetupNullViewFor("DisplayTemplates/String"); var _model = new UserViewModel {Birthday = DateTime.Parse("28/07/11")}; var helper = MvcTestControllerBuilder.GetHtmlHelper<UserViewModel>(); helper.ViewContext.HttpContext.Setup(x => x.Items).Returns(new Dictionary<string, object>()); helper.ViewData.Model = _model; // Action var result = helper.DisplayFor(x => x.Birthday); // Assert Assert.That(result.ToHtmlString(), Is.EqualTo("28/07/11")); }You can refer to my similar posts about MvcTestControllerBuilder (this class has some methods to mock HtmlHelper) and How to mock the ViewEngine. In those posts, I used NSubstitue but now I changed to use Moq, they're quite similar :D. Cheers.
3 comments:
Can you post your project?
I like to test but I have just leaving school, I have no experience to learn. I study testing myself.
Sorry mate, I'm not working on my personal stuff so I'm afraid I can't. However, you can learn about unit test from several weblogs :D or go to Github and checkout some open source projects, that's a very good resource.
Oh, thank you. I hope you have some tutorials on the test.
Post a Comment