Testing is a necessary difficulty that we all must endure. The practice of Test Driven Development (TDD) doesn’t always make testing easier but I firmly believe it makes the development of software more predictable and reliable.
Throughout my career I’ve been a part of many teams, both those disciplined in good testing practices and others not so disciplined. Since my involvement with developing for Sitecore, I have been in the past consistently disappointed in the abilities to test different facets of a Sitecore solution.
There are many articles written on testing and Sitecore. I’ve never been a fan of big setups and tear downs for testing. I like NUnit but like the integration of MSTest and Visual Studio feels so much better to me. I like mocking frameworks and dependency injection along with ASP.NET MVC. I have had clients in the past running tests through a browser context and I’ve never really been a fan of this approach.
In doing some recent Sitecore work I really wanted to get back to using a TDD approach. With Autofac, Moq, Glass Mapper I went forward to see what I could do.
So after creating my MVC project, installing and configuring Glass Mapper to use Autofac, I wrote my first test. For this article, I’ll go through the testing process I did for a simple meta tag rendering use case.
The acceptance criteria of the use case is to write out the following meta tags on each page: keywords, description, and author(s). Keywords and description tags will be populated from fields on the content item, while author will be populated from the Sitecore item created by and updated by user values. We will focus on description and author for the purposes of this post.
I’ve decided to implement these as MVC extensions for my solution. So my tests will be placed in the containing class Extension_Tests.
The first test is a simple test, if the description field has a value, generate the description meta tag on the page. So we start with this…
[TestMethod] public void Extensions_GivenDescriptionisPopulated_ReturnsDescriptionTag() { // arrange var resultsSearchString = "<meta name=\"description\""; // act var result = ProductsExtensions.MetaTags().ToHtmlString(); // assert Assert.IsTrue(result.Contains(resultsSearchString)); }
I set up my tests with method naming conventions that identify the grouping, condition and the expected result. Lengthy but I know exactly what is going on when reviewing a list of test results.
I also follow a pattern of arrange, act and assert to construct the logic of my tests. I’m not going to do a deep dive in this pattern use, but you can read more here.
So now I create stubs of the assets I need to get the application to compile and run my first test. My test fails as expected.
Now we get our act together (like how I did that?) to get the test to pass. We will add the appropriate logic to return our criteria.
public static MvcHtmlString MetaTags() { return new MvcHtmlString("<meta name=\"description\""); }
Yes this seems wrong – but my test passes.
Now we go through the iteration of fulfilling the acceptance criteria of the test. So to take a page out of every cooking show we’ve ever seen – here is what eventually has baked through several iterations of red, green, refactor.
So let’s quickly step through this:
[TestMethod] public void Extensions_GivenDescriptionisPopulated_ReturnsDescriptionTag() { // arrange var page = new Mock<IPageBase>(); page.Setup(p => p.MetaDescription).Returns("I am the great description."); var resultsSearchString = "<meta name=\"description\""; // act var result = ProductsExtensions.MetaTags(_helper, page.Object).ToHtmlString(); // assert Assert.IsNotNull(page); Assert.IsTrue(result.Contains(resultsSearchString)); }
public static MvcHtmlString MetaTags(this HtmlHelper helper, IPageBase content) { var sb = new StringBuilder(); if (!string.IsNullOrEmpty(content.MetaDescription)) { sb.AppendFormat("<meta name=\"description\" content=\"{0}\" />", content.MetaDescription); } return new MvcHtmlString(sb.ToString()); }
So with using Moq you can see in the arrange section of my test I am mocking up a page base and populating it with criteria that supports the test. Why don’t I just perform a setup and grab a Sitecore item? I am assuming the good folks at Sitecore have got a handle on their product and have already tested the item retrieval functions from Sitecore. I only care about testing the code in my extension.
So the extension has developed into a simple if statement instead of the hardcoding in the previous passing test.
In order to call the extensions a helper needed to be created. Because of this I’ve put in a CreateHelper method to be able to properly utilize HtmlHelper into the tests.
[ClassInitialize()] public static void CreateHelper(TestContext testContext) { var viewContext = new Mock<ViewContext>(); var response = new Mock<HttpResponseBase>(); var responseText = string.Empty; response.Setup(r => r.Write(It.IsAny<string>())) .Callback((string s) => responseText += s); viewContext.Setup(v => v.HttpContext.Response).Returns(response.Object); _helper = new HtmlHelper(viewContext.Object, new Mock<IViewDataContainer>().Object); }
Our assert checks as a precaution that the mock object was created and then in our case we check to see if our description meta tag is found. It is not, which is what we expect and so the test passes.
Normally we would go on from here and continue the variations of testing the description meta tag. For brevity however we’ll move on to the author’s test.
The same approach was taken for the author tests. However dependency injection and static classes are like oil and water – they just don’t work well together. Because of needing to inject a class containing testable logic to retrieve item authors, a bigger refactoring took place. Now instead of an extensions class we will be performing a controller rendering for the meta tags. Since this is an architecture change, the expectation is that all test will fail as we make the migration. So now red-green-refactor extends back to the original tests.
public class ProductsPageController : SitecoreController { ISitecoreContext _context; ISitecoreSecurityService _securityService; // constructors for Sitecore and IOC public ProductsPageController() : this(new SitecoreContext(), new SitecoreSecurityService()) { } public ProductsPageController(ISitecoreContext context, ISitecoreSecurityService sitecoreSecurityService) { _context = context; _securityService = sitecoreSecurityService; } public ActionResult MetaTags(IPageBase pageBase) { var sb = new StringBuilder(); var metaOutput = new MetaOutput(); if (!string.IsNullOrEmpty(pageBase.MetaDescription)) { sb.AppendFormat("<meta name=\"description\" content=\"{0}\" />{1}", pageBase.MetaDescription, Environment.NewLine); } // compile authors from created by and updated by var authors = string.Empty; if (string.IsNullOrEmpty(pageBase.MetaAuthor)) { if (!string.IsNullOrEmpty(pageBase.CreatedBy)) { var createdByAuthor = _securityService.GetUserFullName(pageBase.CreatedBy); if(!string.IsNullOrEmpty(createdByAuthor)) { authors = createdByAuthor; } } if (!string.IsNullOrEmpty(pageBase.UpdatedBy)) { var createdByAuthor = _securityService.GetUserFullName(pageBase.UpdatedBy); if (!string.IsNullOrEmpty(createdByAuthor)) { authors += (string.IsNullOrEmpty(authors)) ? createdByAuthor : ", " + createdByAuthor; } } } else { authors = pageBase.MetaAuthor; } if (!string.IsNullOrEmpty(authors)) { sb.AppendFormat("<meta name=\"author\" content=\"{0}\" />", authors); } metaOutput.MetaTags = sb.ToString(); return View(ViewConstants.VIEW_PRODUCTPAGE_METATAGS, metaOutput); } }
[TestMethod] public void Extensions_GivenDescriptionisPopulated_ReturnsDescriptionTag() { // arrange var page = new Mock<IPageBase>(); page.Setup(p => p.MetaDescription).Returns("I am the great description."); var context = new Mock<ISitecoreContext>(); var securityService = new Mock<ISitecoreSecurityService>(); var productsPageController = new ProductsPageController(context.Object, securityService.Object); var resultsSearchString = "<meta name=\"description\""; // act ViewResult result = productsPageController.MetaTags(page.Object) as ViewResult; var model = (IMetaOutput)result.Model; // assert Assert.IsNotNull(result.Model); Assert.IsTrue(model.MetaTags.Contains(resultsSearchString)); } [TestMethod] public void Extensions_GivenDescriptionisEmpty_ReturnsNoDescriptionTag() { // arrange var page = new Mock<IPageBase>(); page.Setup(p => p.MetaDescription).Returns(string.Empty); page.SetupProperty(p => p.MetaApplicationName).Equals("guitars"); page.SetupProperty(p => p.MetaAuthor).Equals("John Doe"); page.SetupProperty(p => p.MetaKeywords).Equals("energy,solar"); var context = new Mock<ISitecoreContext>(); var securityService = new Mock<ISitecoreSecurityService>(); var productsPageController = new ProductsPageController(context.Object, securityService.Object); var resultsSearchString = "<meta name=\"description\""; // act ViewResult result = productsPageController.MetaTags(page.Object) as ViewResult; var model = (IMetaOutput)result.Model; // assert Assert.IsNotNull(page); Assert.IsFalse(model.MetaTags.Contains(resultsSearchString)); } [TestMethod] public void Extensions_GivenAuthorInformationIsAvailable_ResultsInAuthorMetaTag() { // tag looks like this <meta name="author" content="Administrator"> //arrange var context = new Mock<ISitecoreContext>(); var securityService = new Mock<ISitecoreSecurityService>(); var page = new PageBase() { MetaKeywords = "cans,wheat,gas", MetaDescription = "Admin", CreatedBy = @"sitecore\mservais" }; context.Setup(ctx => ctx.GetCurrentItem<IPageBase>(It.IsAny<Boolean>(), It.IsAny<Boolean>())).Returns(page); securityService.Setup(sec => sec.GetUserFullName(It.IsAny<string>())).Returns("Mark Servais"); var productsPageController = new ProductsPageController(context.Object, securityService.Object); var resultsSearchString = "<meta name=\"author\" content=\"Mark Servais\" />"; //act ViewResult result = productsPageController.MetaTags(page) as ViewResult; var model = (IMetaOutput)result.Model; //assert Assert.IsTrue(model.MetaTags.Contains(resultsSearchString)); }
So I’m happy with what I can do here with the testing framework setup. It allows me to make sure I’m building the correct and minimal amount of code needed to accomplish the expected outcome. It also provides me a great source of regression unit testing, and keeps me in the IDE and I don’t necessarily have to run my tests in a browser.
Hi!
A note: The source code in your blog artice is broken for me.
Contains a lot of <span class="…" elements.