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

1<%@ 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

 1using System;  
 2using System.Collections;  
 3using System.Configuration;  
 4using System.Data;  
 5using System.Linq;  
 6using System.Web;  
 7using System.Web.Security;  
 8using System.Web.UI;  
 9using System.Web.UI.HtmlControls;  
10using System.Web.UI.WebControls;  
11using System.Web.UI.WebControls.WebParts;  
12using System.Xml.Linq;  
13using TypeMock.ArrangeActAssert;  
14using Microsoft.SharePoint;  
15using System.Collections.Generic;  
16  
17namespace TestWebSite  
18{  
19    public partial class SpSimpleTest : System.Web.UI.Page  
20    {  
21        public const string ListName = "Test List";  
22  
23        protected void Page\_Load(object sender, EventArgs e)  
24        {  
25            // set the name of the list to read data from  
26            wp1.DataList = ListName;  
27  
28            // set the fake return value for the currently running context  
29            // we can us null as the current parameter as this is what this web page will return  
30            Isolate.WhenCalled(() => Microsoft.SharePoint.WebControls.SPControl.GetContextSite(null).Url).WillReturn("http://mockedsite.com");  
31  
32            // Now the site  
33            SPSite fakeSite = Isolate.Fake.Instance<SPSite>();  
34            Isolate.Swap.NextInstance<SPSite>().With(fakeSite);  
35  
36            var itemCollection = new List<SPListItem>();  
37            for (int i = 0; i < 3; i++)  
38            {  
39                var fakeItem = Isolate.Fake.Instance<SPListItem>();  
40                itemCollection.Add(fakeItem);  
41  
42                Isolate.WhenCalled(() => fakeItem\["Title"\]).WillReturn(string.Format("Title {0}", i));  
43                Isolate.WhenCalled(() => fakeItem\["Email Address"\]).WillReturn(string.Format("email{0}@email.com", i));  
44  
45            }  
46  
47            Isolate.WhenCalled(() => fakeSite.RootWeb.Lists\[ListName\].Items).WillReturnCollectionValuesOf(itemCollection);  
48  
49  
50        }  
51    }  
52}

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.

 1using System;  
 2using System.Collections.Generic;  
 3using System.Linq;  
 4using System.Text;  
 5using TypeMock.ArrangeActAssert;  
 6using Microsoft.SharePoint;  
 7using Microsoft.VisualStudio.TestTools.UnitTesting;  
 8using Ivonna.Framework;  
 9using System.Web.UI.WebControls;  
10using System.Web.UI.WebControls.WebParts;  
11  
12namespace TestProject  
13{  
14    \[TestClass, RunOnWeb\]  
15    public class IvonnaTest  
16    {  
17        public const string ListName = "Test List";  
18  
19        \[TestMethod\]  
20        public void LoadWebPage\_RenderWebPart\_3EntriesInList()  
21        {  
22            // the fake site is now created inside the test not in te aspx page, remember not to fake it twice!  
23            // create the mock SP Site we are using  
24            Isolate.WhenCalled(() => Microsoft.SharePoint.WebControls.SPControl.GetContextSite(null).Url).WillReturn("http://mockedsite.com");  
25            SPSite fakeSite = Isolate.Fake.Instance<SPSite>();  
26            Isolate.Swap.NextInstance<SPSite>().With(fakeSite);  
27  
28            var itemCollection = new List<SPListItem>();  
29            for (int i = 0; i < 3; i++)  
30            {  
31                var fakeItem = Isolate.Fake.Instance<SPListItem>();  
32                itemCollection.Add(fakeItem);  
33  
34                Isolate.WhenCalled(() => fakeItem\["Title"\]).WillReturn(string.Format("Title {0}", i));  
35                Isolate.WhenCalled(() => fakeItem\["Email Address"\]).WillReturn(string.Format("email{0}@email.com", i));  
36  
37            }  
38  
39            Isolate.WhenCalled(() => fakeSite.RootWeb.Lists\[ListName\].Items).WillReturnCollectionValuesOf(itemCollection);  
40  
41            TestSession session = new TestSession(); //Start each test with this  
42            WebRequest request = new WebRequest("SpMvcTest.aspx"); //Create a WebRequest object  
43            WebResponse response = session.ProcessRequest(request); //Process the request  
44            System.Web.UI.Page page = response.Page;  
45            //Check the page loaded  
46            Assert.IsNotNull(page);  
47  
48            // you would hope you could get to a given cntrol using th efollowing lines  
49            // but they do not work  
50            //var txt = page.FindControl("WebPartManager1$wp1$ctl09");  
51            //var txt = page.FindControl("WebPartManager1\_wp1\_ctl09");  
52  
53            // check the webpart, we have to get at this via the zone  
54            WebPartZone wpzone = page.FindControl("WebPartZone1") as WebPartZone;  
55            Assert.IsNotNull(wpzone);  
56            var wp = wpzone.WebParts\[0\] as DemoWebParts.SpMvcWebPart;  
57            Assert.IsNotNull(wp);  
58  
59            // so we have to use the following structure and dig knowing the format  
60            // webpart/panel/table/row/cell/control  
61            var txt = ((TableRow)wp.Controls\[0\].Controls\[0\].Controls\[0\]).Cells\[1\].Controls\[0\] as Label;  
62            Assert.IsNotNull(txt);  
63            Assert.AreEqual("http://mockedsite.com", txt.Text);  
64  
65            var list = ((TableRow)wp.Controls\[0\].Controls\[0\].Controls\[1\]).Cells\[1\].Controls\[0\] as DropDownList;  
66            Assert.IsNotNull(list);  
67            Assert.AreEqual(3, list.Items.Count);  
68  
69        }  
70  
71    }  
72}

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.

1\[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.