But it works on my PC!

The random thoughts of Richard Fennell on technology and software development

Developer testing of Sharepoint Webparts using Typemock Isolator and Ivonna

Updated 3 Dec 2008 – I got an email from Artem Smirnov the author of Ivonna pointing out a couple of things, so I have updated this post
Updated 3 May 2009 – I altered the code samples as the previous ones did not seem to work with Typemock Isolator 5.3.0 .

I have previously written a post on using Isolator with Sharepoint, also Andrew Woodward has written a good and more detailed tutorial on the subject, so I don’t intend to go over old ground here.

Want I want to look in this post is the testing of webparts. A webpart, whether in Sharepoint or not is fundamentally a data viewer, something is rendered to HTML. As a developer a good deal of time is spent making sure what is rendered is what is required. Usually this means making sure the correct controls are rendered and the right CSS applied. Now due to the Sharepoint deploy model the process of editing the webpart, compiling it, building a WSP or manually deploying can be slow; often requiring the use of a VPC based development system. In this post I discuss ways to mitigate these problems.

If there are no calls to Sharepoint

If your webpart makes no reference to the Sharepoint object model you can write an ASP.NET test harness to load the webpart as below

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="BasicTest.aspx.cs" Inherits="TestWebSite.BasicTest" %>

<%@ Register Assembly="DemoWebParts" Namespace="DemoWebParts" TagPrefix="wp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:TextBox ID="textbox1" runat="server" Text="Demo text" />
        <asp:WebPartManager ID="WebPartManager1" runat="server">
        </asp:WebPartManager>
        <asp:WebPartZone ID="WebPartZone1" runat="server" >
            <ZoneTemplate>
                <wp:HelloWorldWebPart id="wp1" runat="server" />
            </ZoneTemplate>
        </asp:WebPartZone>
    </div>
    </form>
</body>
</html>
This means I can loading the page with the webpart as fast as any other ASP.NET page and do whatever manual tests I want to do. However, this technique does not work if you need to get data from Sharepoint.

Mocking out Sharepoint

To address the case when I have to get data from Sharepoint I have been using Typemock Isolator inside the ASP.NET page load. As shown below

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using TypeMock.ArrangeActAssert;
using Microsoft.SharePoint;
using System.Collections.Generic;

namespace TestWebSite
{
public partial class SpSimpleTest : System.Web.UI.Page
{
public const string ListName = "Test List";

protected void Page_Load(object sender, EventArgs e)
{
// set the name of the list to read data from
wp1.DataList = ListName;

// set the fake return value for the currently running context
// we can us null as the current parameter as this is what this web page will return
Isolate.WhenCalled(() => Microsoft.SharePoint.WebControls.SPControl.GetContextSite(null).Url).WillReturn("http://mockedsite.com");

// Now the site
SPSite fakeSite = Isolate.Fake.Instance<SPSite>();
Isolate.Swap.NextInstance<SPSite>().With(fakeSite);

var itemCollection = new List<SPListItem>();
for (int i = 0; i < 3; i++)
{
var fakeItem = Isolate.Fake.Instance<SPListItem>();
itemCollection.Add(fakeItem);

Isolate.WhenCalled(() => fakeItem["Title"]).WillReturn(string.Format("Title {0}", i));
Isolate.WhenCalled(() => fakeItem["Email Address"]).WillReturn(string.Format("email{0}@email.com", i));

}

Isolate.WhenCalled(() => fakeSite.RootWeb.Lists[ListName].Items).WillReturnCollectionValuesOf(itemCollection);


}
}
}

In effect I do the same as I did in the previous post but have placed the fake object creation in the page load. Now when I tried this with Typemock 5.1.2 it did not work, so I put a query on the product forum and turns out there was a namespace configuration file issue. Typemock quickly issued a patch which I am told will be included in 5.1.3.

Updated 3 May 2009 I altered this code sample as the previous form had worked with 5.1.3 did not work with 5.3.0. This new form of faking should be OK with all versions.

So with this setup we can place a Sharepoint dependant webpart in a test ASP.NET page and get it to render, thus again making for a fast development/design/manual test framework that does not require Sharepoint to be installed on the development PC. Great for sorting out all those CSS issues.

Mocking out the web server too

However in  a TDD world it would be nice to automate some of the webpart testing, so we could encode a question like ‘if there are three items in a sharepoint list does the webpart renders a combo box with three items in it?’.

Now the purest might say this is not a unit test, it is an integration test. I am coming to the conclusion that especially in the land of Sharepount this semantic difference is not worth arguing about as all test tend to integration. For this reason I tend to think more of developer tests as opposed to acceptance tests – developer tests being the ones the developer can run repeatedly in the TDD style, during the development and refactor process, as opposed to slow tester that are part of the automated build or QA process.

So to this end I have been looking at Ivonna. This allows, using Typemock beneath it, the developer to create a mock webserver. So you can programmatically in a test load a web page that holds a webpart (I use the same test page/site I used above), press some buttons etc and probe the contents of the webpart.

You end up with tests that look like this

Updated 3 May 2009 Again I altered this code sample to work for Isolator 5.3.0.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TypeMock.ArrangeActAssert;
using Microsoft.SharePoint;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Ivonna.Framework;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;

namespace TestProject
{
[TestClass, RunOnWeb]
public class IvonnaTest
{
public const string ListName = "Test List";

[TestMethod]
public void LoadWebPage_RenderWebPart_3EntriesInList()
{
// the fake site is now created inside the test not in te aspx page, remember not to fake it twice!
// create the mock SP Site we are using
Isolate.WhenCalled(() => Microsoft.SharePoint.WebControls.SPControl.GetContextSite(null).Url).WillReturn("http://mockedsite.com");
SPSite fakeSite = Isolate.Fake.Instance<SPSite>();
Isolate.Swap.NextInstance<SPSite>().With(fakeSite);

var itemCollection = new List<SPListItem>();
for (int i = 0; i < 3; i++)
{
var fakeItem = Isolate.Fake.Instance<SPListItem>();
itemCollection.Add(fakeItem);

Isolate.WhenCalled(() => fakeItem["Title"]).WillReturn(string.Format("Title {0}", i));
Isolate.WhenCalled(() => fakeItem["Email Address"]).WillReturn(string.Format("email{0}@email.com", i));

}

Isolate.WhenCalled(() => fakeSite.RootWeb.Lists[ListName].Items).WillReturnCollectionValuesOf(itemCollection);

TestSession session = new TestSession(); //Start each test with this
WebRequest request = new WebRequest("SpMvcTest.aspx"); //Create a WebRequest object
WebResponse response = session.ProcessRequest(request); //Process the request
System.Web.UI.Page page = response.Page;
//Check the page loaded
Assert.IsNotNull(page);

// you would hope you could get to a given cntrol using th efollowing lines
// but they do not work
//var txt = page.FindControl("WebPartManager1$wp1$ctl09");
//var txt = page.FindControl("WebPartManager1_wp1_ctl09");

// check the webpart, we have to get at this via the zone
WebPartZone wpzone = page.FindControl("WebPartZone1") as WebPartZone;
Assert.IsNotNull(wpzone);
var wp = wpzone.WebParts[0] as DemoWebParts.SpMvcWebPart;
Assert.IsNotNull(wp);

// so we have to use the following structure and dig knowing the format
// webpart/panel/table/row/cell/control
var txt = ((TableRow)wp.Controls[0].Controls[0].Controls[0]).Cells[1].Controls[0] as Label;
Assert.IsNotNull(txt);
Assert.AreEqual("http://mockedsite.com", txt.Text);

var list = ((TableRow)wp.Controls[0].Controls[0].Controls[1]).Cells[1].Controls[0] as DropDownList;
Assert.IsNotNull(list);
Assert.AreEqual(3, list.Items.Count);

}

}
}

Update after email from Artem

  • In the code sample I have used the long winded way of loading a page, for clarity of what is going on , but you could just write 

    System.Web.UI.Page page = session.GetPage("SpMvcTest.aspx")

  • I stated you cannot write

    var txt = page.FindControl("WebPartManager1$wp1$ctl09");

    this is a limitation/feature of Asp.Net naming containers, not Ivonna's, but you can write

    var wp = (new ControlHelper(page)).FindControl("wp1") as DemoWebParts.SpMvcWebPart;

    and in the version 1.2.0 of Ivonna you can use an extension method:

    var wp = page.FindRecursive<DemoWebParts.SpMvcWebPart>("wp1");

    If you are sure about the "ctl09" id (that could change if you change the layout), you can also write:

    var txt = (new ControlHelper(page)).FindControl("WebPartManager1", "wp1", "ctl09") as Label

    so that it looks for something with ID of "ctl09" inside something with ID of "wp1" inside something with ID of "WebPartManager1"

There are a couple of gottas with this system

  • the ‘path’ to the controls within the test page are a little nasty, you don’t seem to be able to just use FindControl(string); but you should know what you are after so it is not that limiting.
  • you have to hard code the webpart into the test page. In theory you could programmatically add them, but this would require a personalisation provider running behind the WebPart manager which in turn would require a SQL provider so not realistic for a test in a mock framework (maybe we could mock this too?). Again I don’t see this as a major limit.

A better design

Up to this point I have been assuming a very naively written webpart with all the logic in the CreateChildControls method and behind button events. Without Typemock and Ivonna this is all but un-testable, but I hope I have shown we now have options to develop and test outside Sharepoint.

At this point I think it is important to also consider a better design for the webpart. Using an MVC model we get many more potential points to test. Now it is an interesting discussion if a webpart can be MVC, as MVC is a design for a whole page (and associated underlying framework) not just a small part of a page. However we can use the basic design principles of separation of roles in MVC thus allow all our Sharepoint calls to be placed in the Sharepoint implementation of some IDataprovider model, which we could manually mock out etc.

This is all good, allowing manual mocking via dependency injection, but again we can use Typemock to dynamically mock out the models, view or controller, or just to duck type items thus creating tests as shown below. This should be a great saving in time and effort.

[TestMethod]
public void WebPartController_LoadFromSharePointIntoManuallyMockedView_Returns3Items()
{

    TestHelpers.CreateFakeURL();
    TestHelpers.CreateFakeSPSite();

    var datalayer = new DemoWebParts.Models.SPDataSource(
        Microsoft.SharePoint.WebControls.SPControl.GetContextSite(null).Url,
        TestHelpers.ListName);

        
    // the controller will create a view, the fake should be swapped in
    var controller = new DemoWebParts.Controllers.Controller(datalayer);

    // create a local view that expose data for test
    TestView view = new TestView();
    // and swap it in
    Isolate.Swap.CallsOn(controller.View).WithCallsTo(view);


    // get the data, there should be data in the view
    controller.Init();


    // check the data is there as expected
    Assert.AreEqual(3, view.TestData.EmailAddresses.Count);


}

So in summary, if you are looking at Sharepoint and, as I have, wondered how to test or to speed up your developer/test cycle have a serious Typemock and Ivonna. I think you will like what you find.

Pingbacks and trackbacks (4)+

Comments are closed