New Technique for Unit Testing Renderings in Sitecore

10

April 13, 2010 by Alistair Deneys

Unit testing in Sitecore always seems to be a hot topic amongst the Sitecore community, especially the new comers to the Sitecore game. And rightly so. Working out how you can effectively unit test your Sitecore code can be very confusing when you’re still trying to come to grips with a new system and it’s way of doing things. And it’s not immediately apparent how you can do such testing.

I think the biggest key to being able to write effective unit tests with Sitecore is to give up the notion of proper unit testing. Roy Osherove (unit testing guru) recently wrote what I think is the best and most concise definition of what a unit test is and what it should do:

A unit test is a fast, in-memory, consistent, automated and repeatable test of a functional unit-of-work in the system.

Given the above definition we can easily see that almost any test we write for code which uses the Sitecore API cannot be a unit test, especially if we’re using any data (Items) from Sitecore as this API will pull the data from an external database engine. So the biggest key to writing effective tests with Sitecore is to call this subject automated testing, not unit testing. We can’t isolate the test to a point where the entire thing runs in memory, so don’t worry about trying. It’s more important to have an automated, reliable, repeatable suite of tests you can run at the click of a button.

The other barrier a lot of people face when trying to write automated tests for code which uses the Sitecore API is trying to have your tests run outside of ASP.NET. Sitecore is an ASP.NET application. It relies on HttpContext and the ASP.NET request lifecycle and rendering engine not to mention the giant configuration file required as well. I’ve never actually heard of anyone successfully running Sitecore API code outside of ASP.NET (let me know if you have). This is a huge barrier to a lot of people who think their tests have to run inside their chosen unit testing framework’s test runner. This is not the case.

The easiest, quickest and most reliable way around this is to simply have your tests run inside Sitecore. I’ve written and released an automated test runner for NUnit tests which can be used to run your own tests inside your Sitecore application. This is not an extraordinary circumstance. The SilverUnit project provides a unit testing framework inside of Silverlight. So don’t feel like this concept of running testing inside the application is too foreign.

I wrote an 8 part series on automated testing with Sitecore a while back. The rundown of the series was as follows:

Since writing this series I’ve discovered a new technique for testing your renderings. This has also revealed a short coming in the technique shown in part 5 above for testing WebControls. You’ll see why this is later on in this post.

The basic process for this new technique is:

  • Instantiate rendering in test code
  • Set the context item to test rendering against
  • Gather output and use HtmlAgilityPack to construct an XML document from it
  • Verify your output
    The above of course needs to run inside Sitecore, so I’ll use my custom test runner. A nuance of the current test runner, your test fixture needs to be attributed with a NUnit.Framework.CategoryAttribute attribute. This is so the test runner can produce a list of categories to allow you to only test a small selection from your entire test suite for a particular test run. The tests run are those inside the selected categories.

Follow these steps to setup your test project:

  • Grab yourself a copy of the NUnit libraries from http://www.nunit.org/ or simply use the NUnit libraries bundled with the item below.
  • Download my custom NUnit test runner from http://users.tpg.com.au/adeneys/codeNUnitRunner.html.
  • Create your test project and include a reference to the NUnit.Core, NUnit.Core.Interfaces and NUnit.Framework assemblies.
  • Copy the 3 test runner source files (aspx, aspx.cs and designer.cs files) into your project folder and include in the project.
  • Change the namespace of the test runner code behind and the titles in the aspx file.

The test runner has been written to run the tests in the same assembly as the runner, so you can write your tests in the same project.

Now, let’s create a new test fixture that we can write tests for the rendering under inspection.

[TestFixture]
[Category("NavTest")]
public class NavTest
{
    [TestFixtureSetUp]
    public void TestFixtureSetUp()
    {
    }

    [TestFixtureTearDown]
    public void TestFixtureTearDown()
    {
    }
}

When testing a rendering you need to be sure of the items it’s reading, so we’ll need to create the test content items the rendering will be run against. These will be created in the TestFixtureSetUp method. And of course this test suite would be bad if we didn’t return the environment back to the way we found it before we started our test, so we’ll delete the test items in the TestFixtureTearDown method. The attributes on these classes tell NUnit to run TestFixtureSetUp before any test is run (preparing the test environment) and run TestFixtureTearDown after the tests have all completed (returning the test environment to it’s previous state).

private Item m_root, m_child1, m_child2, m_child3 = null;

[TestFixtureSetUp]
public void TestFixtureSetUp()
{
    var home = Sitecore.Context.Database.GetItem("/sitecore/content/home");
    var template = Sitecore.Context.Database.Templates["User Defined/Page"];

    using (new SecurityDisabler())
    {
        m_root = home.Add("root", template);

        m_child1 = m_root.Add("Child 1", template);
        m_child1.Editing.BeginEdit();
        m_child1["title"] = "Child 1 Title";
        m_child1.Editing.EndEdit();

        m_child2 = m_root.Add("Child 2", template);
        m_child3 = m_root.Add("Child 3", template);
    }
}

[TestFixtureTearDown]
public void TestFixtureTearDown()
{
    using (new SecurityDisabler())
    {
        m_root.Delete();
    }
}

I’ve stored the items in member variables so I can make my assertions against them.

Now that we’ve got our test all setup we can start testing the rendering. The rendering I’ll be testing in the below code is a basic child navigation control. The text of each link should be taken from the title field of the item. If that field is empty, then the name should be used. So I’ll be testing 2 conditions. The first is the inclusion of the correct items in the navigation (the children), and the second is the logic around the text of the link. In the above setup code I’ve set a title on child 1 which is different to it’s name, so I can test the link text logic using that item.

And so onto the initial rendering.

<xsl:template match="*" mode="main">
  <ul>
    <xsl:for-each select="child::item">
      <li>
        <sc:link/>
      </li>
    </xsl:for-each>
  </ul>
</xsl:template>

You’ll notice the above XSLT code contains an error in the way it generates the text of the link. The sc:link tag will use the name of the item for the link text. This way we get to see our test fail the first time so we know it’s actually working.

Onto some tests to verify our rendering. The first will be for item 1 where I’m testing it’s inclusion and the link text.

[Test]
public void InclusionAndTitle()
{
    var xslFile = new XslFile();
    xslFile.Path = "xsl/Nav.xslt";

    Sitecore.Context.Item = m_root;

    var output = xslFile.RenderAsText();
    var doc = new HtmlDocument();
    doc.LoadHtml(output);

    var url = LinkManager.GetItemUrl(m_child1);
    var nav = doc.CreateNavigator().SelectSingleNode(string.Format("//a[@href='{0}']", url));

    var linkText = nav.Value;
    Assert.AreEqual("Child 1 Title", linkText);
}

For tests 2 and 3 I’ll pretty much just be replicating this same code, so I’ll refactor it so I can write less code.

[Test]
public void InclusionAndTitle()
{
    var linkText = GetLinkText(m_root, m_child1);
    Assert.AreEqual("Child 1 Title", linkText);
}

[Test]
public void InclusionAndName()
{
    var linkText = GetLinkText(m_root, m_child2);
    Assert.AreEqual("Child 2", linkText);
}

[Test]
public void InclusionAndName2()
{
    var linkText = GetLinkText(m_root, m_child3);
    Assert.AreEqual("Child 3", linkText);
}

private string GetLinkText(Item renderContextItem, Item textItem)
{
    var xslFile = new XslFile();
    xslFile.Path = "xsl/Nav.xslt";

    Sitecore.Context.Item = renderContextItem;

    var output = xslFile.RenderAsText();
    var doc = new HtmlDocument();
    doc.LoadHtml(output);

    var url = LinkManager.GetItemUrl(textItem);
    var nav = doc.CreateNavigator().SelectSingleNode(string.Format("//a[@href='{0}']", url));
    return nav.Value;
}

Running the above tests I would expect test 1 would fail as the text is not as expected.

image

Now we can rework the XSLT code and correct the bug.

<xsl:template match="*" mode="main">
  <ul>
    <xsl:for-each select="child::item">
      <li>
        <sc:link/>
          <xsl:apply-templates mode="renderTitle" select="."/>
        </sc:link>
      </li>
    </xsl:for-each>
  </ul>
</xsl:template>

<xsl:template match="item" mode="renderTitle">
  <xsl:choose>
    <xsl:when test="sc:fld('title',.) != ''">
      <sc:text field="title"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="@name"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Now if you’ve inspected the test code above in the GetLinkText method you might be thinking you could adjust the code and test sublayouts and Sitecore web controls using this same technique. Well, you might be able to. Kind of.

One thing to note about how the code is tested is that the normal ASP.NET request lifecycle is not followed. We are actually half way through the request lifecycle inside the test runner which is run inside the ASP.NET application so it’s invoked through an HTTP request. This just means that some things don’t run. For example, I was successful using this technique for testing a Sitecore web control, but only the DoRender method is called during the test. All the other events such as OnLoad don’t get called. So as long as your Sitecore web control doesn’t require the events to be fired then you’ve got a very good chance of using this technique for testing. Below is an example of gathering the output of a Sitecore web control.

var c = new Banner();
Sitecore.Context.Item = m_grandgrandchild;
var output = c.RenderAsText();

This code is much simpler than that for a rendering cause we can instantiate the web control directly rather than going through another level of indirection as we do with a rendering.

I was much less successful testing sublayouts. Although the Sitecore API calls worked, when using a template control like a repeater, the repeater didn’t render correctly. If your sublayout is very basic you might be able to use this technique, but you’ll also need to call the Expand method on the sublayout instance before the call to RenderAsText.

var sublayout = new Sublayout();
sublayout.Path = "layouts/list.ascx";

Sitecore.Context.Item = renderContextItem;

sublayout.Expand();

var output = sublayout.RenderAsText();
var doc = new HtmlDocument();
doc.LoadHtml(output);

So I hope this technique makes it easier for you to test your Sitecore presentation components.

About these ads

10 thoughts on “New Technique for Unit Testing Renderings in Sitecore

  1. Lars Nielsen says:

    Very very cool.

    As always, a true pleasure to read your posts.

  2. John Wallace says:

    Many thanks for providing the code for the test runner. It’s great!

    I found that test tests are executed twice. Have you experienced this?

    • Alistair Deneys says:

      Hi John,
      No, Can’t say I’ve seen that. Have you modified your test runner code? If you send me the test runner and a few same tests I’ll give it a run and see if I get the same results. You can send me the code at admin@codeflood.net

      • John Wallace says:

        Hi Alistair,
        Thanks for replying. While trying to put together a small test case I found the problem.

        Regarding what I changed, I’m using version 2.5.5.10112 of the NUnit libraries, so I had to change the SuiteFinished() and TestFinished() methods to receive a TestResult parameter since the previous parameters are not available in this version. I also modified the code to execute tests from a different assembly by replacing Assembly.GetExecutingAssembly().Location with the location of the assembly.

        That was all I changed and the code works great. Thanks for posting it.

        The problem was with test cases that use the NUnit TestCase attribute. Every time the runner executes, additional methods seem to be generated for each specified TestCase. So methods that use the TestCase attribute get run twice, three times, and four times, etc. each successive time that the runner is executed.

        For now, I’ll avoid using this attribute. I’ll update you if I find a resolution to the issue.

  3. Preeti says:

    Hi Alistair,

    I have included ‘CustomItemGenerator’ to my Sitecore project and referenced the respective classes/library generated by CustomItemGenerator in the NUnit project.

    When I access the properties from the classes generated by CustonItemGenerator,
    - it works fine with the code you have mentioned in the above article, I am able to access the properties on aspx page
    - but when I access these properties through NUnit GUI (NUnit version 2.5.10) it throws error of ‘Object reference not set…’.

    It would be great if you could provide me any help on this problem.

    • Alistair Deneys says:

      Hi Preeti,
      I would say the issue is because you don’t have an HttpContext when running your tests in the NUnit GUI. There are ways to work around that which I’m planning to write about in an upcoming blog post, so stay tuned.

      If you can’t wait that long, check out my sample project from my Dreamcore talk on unit testing http://adeneys.wordpress.com/2011/10/06/dreamcore-au-2011/. I cover in that how to test against the Sitecore API without an HttpContext, though because it was a presentation there’s not a great deal of explanation. The upcoming blog post will cover that.

      • Preeti says:

        Hi Alistair,

        Following is the stack trace of error I am getting while running the tests (which is accessing properties which are created through ‘CustomItemGenerator’) in NUnit GUI -

        at Sitecore.Pipelines.RenderField.RenderFieldArgs..ctor()
        at Sitecore.Web.UI.WebControls.FieldRenderer.RenderField()
        at Sitecore.Web.UI.WebControls.FieldRenderer.Render()
        at CustomItemGenerator.Fields.BaseCustomField`1.get_Rendered()
        at CustomItemGenerator.Fields.SimpleTypes.CustomTextField.get_Text()

        When I had created my own property in the same class that is accessible in NUnit GUI that but the property which is created through ‘CutomItemGenerator’ is unaccessible. It would be great if you can provide some help on this.

      • Alistair Deneys says:

        Hi Preeti,
        The RenderFieldArgs class uses the context site. Perhaps try populating the context site before calling the RenderAsText method.

  4. [...] One of the important discipline of programming to create your applications using a TDD approach, even though Sitecore makes this hard there are a few things out there to us to help run unit tests.  One example is the test runner that Alistair Deneys created back in 2010 http://adeneys.wordpress.com/2010/04/13/new-technique-for-unit-testing-renderings-in-sitecore [...]

  5. [...] Deneys hat eine sehr schöne Reihe von Posts zum Thema “Unit-Testing mit Sitecore” veröffentlicht. Im Buch “Professional Sitecore Development” wurde ausserdem ein [...]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

The views expressed on this blog are solely my own and do not necessarily reflect the views of my employer.
Follow

Get every new post delivered to your Inbox.

Join 38 other followers

%d bloggers like this: