December 2008 - Posts
If you have TypeMock Isolator based MSTests in a solution you will want them to be run as part of any CI build process.
To get this to work with Team Build you have to make sure Isolator is started in the build box at the right time (something that is done automagically behind the scenes by Visual Studio during developer testing). This is not actually that difficult as TypeMock provide some tasks for just this purpose.
Firstly you have to install Isolator on the build box (and of course license it). Then edit your tfsbuild.proj build script to include the overrides for the beforetest and aftertest targets
<!-- Import the Typemock list of tasks -->
<PropertyGroup>
<TypeMockLocation>C:\Program Files\Typemock\Isolator\5.1</TypeMockLocation>
</PropertyGroup>
<Import Project ="$(TypeMockLocation)\TypeMock.MSBuild.Tasks"/>
<!-- Before the tests are run start TypeMock -->
<Target Name="BeforeTest">
<TypeMockStart/>
</Target>
<!-- And stop it when the are finished -->
<Target Name="AfterTest">
<TypeMockStop/>
</Target>
Once this is done your test should run OK
One of the cool feature of the last October 08 release of TFS Power Tools has been that the members of a Team Project are shown inside Team Explorer.
One of the ideas of this is that you can use Live Messenger from inside Team Explorer to see team members status, but I and many other were seeing the error shown below as Team Explorer refreshed

There had been much talk of it being settings in the registry, UAC being used etc. but none of the fixes detailed worked for me.
However, today it has all started working after I updated to the new version of Live Messenger released in the past few days, Version 2009 (Build 14.0.8050.1202). I suspect the problem in my case was that I was using a beta version of Live Messenger
I posted a while ago about trying to wire in the results from StyleCop into a Team Build, the problem I had was that I could not get the StyleCop violations into the build summary.
Well I still can’t, after much checking and asking around I was reliably informed that the build summary is not editable and there are no immediate plans for it to be in the future versions of TFS.
However, Martin Woodward, another Team System MVP, made the suggestion to add the violation information into the build information object. This would not allow the information to be seen in the build summary in Visual Studio, but it would allow me to programmatically recover it from the IBuildInformation object inside my build wallboard application – which shows the current state of all our current CI Team Builds, it shows a scrolling list row as below
Where the key items are:
- Big graphic showing Building, Success, Partial Success or Failure
- The name of the build and the time it finished
- CE – Compiler errors
- CW – Compiler warnings
- FW – FXCop warnings
- SW – StyleCop violations
- TP – Tests passed
- TF – Test failed
- and the rabbit shows if the build status is reported by a NazBazTag Build Bunny
So to do this I had to write an MSBuild Task, but the code fairly simple as Martin had suggested
//-----------------------------------------------------------------------
// <copyright file="StyleCopResultsMerge.cs" company="Black Marble">
// Black Marble Copyright 2008
// </copyright>
//-----------------------------------------------------------------------
namespace BlackMarble.MSBuild.CodeQuality
{
using System;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Client;
/// <summary>
/// Merges the Stylecop results into the build results for TFS
/// </summary>
public class StyleCopResultsMerge : Task
{
/// <summary>
/// The tfs server to report to
/// </summary>
private TeamFoundationServer tfs;
/// <summary>
/// The build server doing the work
/// </summary>
private IBuildServer buildServer;
/// <summary>
/// The current build
/// </summary>
private IBuildDetail build;
/// <summary>
/// Gets or sets the Url of the Team Foundation Server.
/// </summary>
[Required]
public string TeamFoundationServerUrl
{
get;
set;
}
/// <summary>
/// Gets or sets the Uri of the Build for which this task is executing.
/// </summary>
[Required]
public string BuildUri
{
get;
set;
}
/// <summary>
/// Gets or sets the number of stylecop violations found.
/// </summary>
[Required]
public int Violations
{
get;
set;
}
/// <summary>
/// Gets or sets the number of files stylecop failed to parser.
/// </summary>
[Required]
public int Failures
{
get;
set;
}
/// <summary>
/// Gets the lazy init property that gives access to the TF Server specified by TeamFoundationServerUrl.
/// </summary>
protected TeamFoundationServer Tfs
{
get
{
if (this.tfs == null)
{
if (String.IsNullOrEmpty(this.TeamFoundationServerUrl))
{
// Throw some exception.
}
this.tfs = TeamFoundationServerFactory.GetServer(this.TeamFoundationServerUrl);
}
return this.tfs;
}
}
/// <summary>
/// Gets the lazy init property that gives access to the BuildServer service of the TF Server.
/// </summary>
protected IBuildServer BuildServer
{
get
{
if (this.buildServer == null)
{
this.buildServer = (IBuildServer)this.Tfs.GetService(typeof(IBuildServer));
}
return this.buildServer;
}
}
/// <summary>
/// Gets the lazy init property that gives access to the Build specified by BuildUri.
/// </summary>
protected IBuildDetail Build
{
get
{
if (this.build == null)
{
this.build = (IBuildDetail)this.BuildServer.GetBuild(new Uri(this.BuildUri), null, QueryOptions.None);
}
return this.build;
}
}
/// <summary>
/// ITask implementation - Execute method.
/// </summary>
/// <returns>
/// True if the task succeeded, false otherwise.
/// </returns>
public override bool Execute()
{
try
{
IBuildInformation info = this.Build.Information;
Log.LogMessage("StyleCopResultsMerge for build {0} with {1} violations ", this.Build.Uri.ToString(), this.Violations.ToString());
IBuildInformationNode infoNode = info.CreateNode();
infoNode.Type = "org.stylecop";
infoNode.Fields.Add("total-violations", this.Violations.ToString());
info.Save();
return true;
}
catch (Exception ex)
{
Log.LogError(ex.Message);
return false;
}
}
}
}
This can then wired into the build process I detailed in the older post and have repeated below with the new additions. The choice you have to make is if StyleCop violations will cause the build to fail or not – both are detailed below.
<!-- the imports needed -->
<Import Project="$(MSBuildExtensionsPath)\ExtensionPack\MSBuild.ExtensionPack.tasks"/>
<!-- this could be a task file if you wanted -->
<UsingTask AssemblyFile="$(BMTasksPath)BlackMarble.MSBuild.CodeQuality.StyleCopResultsMerge.dll" TaskName="BlackMarble.MSBuild.CodeQuality.StyleCopResultsMerge"/>
<!-- All the other Target go here -->
<Target Name="AfterCompile">
<!-- Create a build step to say we are starting StyleCop -->
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Name="StyleCopStep"
Message="StyleCop step is executing.">
<Output TaskParameter="Id" PropertyName="StyleCopStep" />
</BuildStep>
<!-- Create a collection of files to scan, ** means and sub directories -->
<CreateItem Include="$(SolutionRoot)\My Project\**\*.cs">
<Output TaskParameter="Include" ItemName="StyleCopFiles"/>
</CreateItem>
<!-- Run the StyleCop MSBuild Extensions task using the setting file in the same directory as sln file and also stored in TFS -->
<MSBuild.ExtensionPack.CodeQuality.StyleCop
TaskAction="Scan"
SourceFiles="@(StyleCopFiles)"
ShowOutput="true"
ForceFullAnalysis="true"
CacheResults="false"
logFile="$(DropLocation)\$(BuildNumber)\StyleCopLog.txt"
SettingsFile="$(SolutionRoot)\My Project\Settings.StyleCop"
ContinueOnError="false">
<Output TaskParameter="Succeeded" PropertyName="AllPassed"/>
<Output TaskParameter="ViolationCount" PropertyName="Violations"/>
<Output TaskParameter="FailedFiles" ItemName="Failures"/>
</MSBuild.ExtensionPack.CodeQuality.StyleCop>
<!-- Run the new results merge task -->
<BlackMarble.MSBuild.CodeQuality.StyleCopResultsMerge
TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Violations="$(Violations)"
Failures ="0"
/>
<!-- Put up a message in the build log to show results irrespective of what we do next -->
<Message Text="StyleCop Succeeded: $(AllPassed), Violations: $(Violations)"/>
<!-- FailedFile format is:
<ItemGroup>
<FailedFile Include="filename">
<CheckId>SA Rule Number</CheckId>
<RuleDescription>Rule Description</RuleDescription>
<RuleName>Rule Name</RuleName>
<LineNumber>Line the violation appears on</LineNumber>
<Message>SA violation message</Message>
</FailedFile>
</ItemGroup>-->
<Warning Text="%(Failures.Identity) - Failed on Line %(Failures.LineNumber). %(Failures.CheckId): %(Failures.Message)"/>
<!-- The StyleCop task does not throw an error if the analysis failed,
so we need to check the return value and if we choose to treat errors as warnngs
we need to set the error state -->
<Error Text="StyleCop analysis warnings occured" Condition="'$(AllPassed)' == 'False'" />
<!-- List out the issues, you only need this if we are not forcing the error above -->
<!--<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Message="%(Failures.Identity) - Failed on Line %(Failures.LineNumber). %(Failures.CheckId): %(Failures.Message)"/>-->
<!-- Log the fact that we have finished the StyleCop build step, as we had no error -->
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(StyleCopStep)"
Status="Succeeded"
Message="StyleCop Succeeded: $(AllPassed), Violations: $(Violations)"/>
<!-- If an error has been raised we call this target
You might have thought you could so the same as the error line above and this followng
OnError line by adding a condition as shown below. However this does not work
as the OnError condition is not evaluated unless an error as previously occured-->
<OnError ExecuteTargets="FailTheBuild" />
<!--<OnError ExecuteTargets="FailTheBuild" Condition="'$(AllPassed)' == 'False'" />-->
</Target>
<Target Name="FailTheBuild">
<!-- We are failing the build due to stylecop issues -->
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Id="$(StyleCopStep)"
Status="Failed"
Message="StyleCop Failed: $(AllPassed), Violations: $(Violations) [See $(DropLocation)\$(BuildNumber)\StyleCopLog.txt]"/>
<!-- List out the issues-->
<BuildStep TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
BuildUri="$(BuildUri)"
Message="%(Failures.Identity) - Failed on Line %(Failures.LineNumber). %(Failures.CheckId): %(Failures.Message)"/>
</Target>
Finally to get the new in formation out of the build and into the build wallboard
public void UpdateStatus(IBuildDetail detail)
{
// any other results fields updates
// and now the custom nodes
IBuildInformation info = detail.Information;
foreach (IBuildInformationNode infoNode in info.Nodes)
{
if (infoNode.Type == "org.stylecop")
{
// we have the correct node
this.SetStyleCopWarnings(infoNode.Fields["total-violations"]);
break;
}
}
}
So not a perfect solution, but but does everything I need at present.
If your team build project uses the Publish Target option (to create a ClickOnce deploy) you may see the error
BuildWallboard.csproj" (Publish target) (3:5) ->
(_DeploymentGenerateBootstrapper target) ->
MSB3155: Item 'Microsoft.Net.Framework.3.5.SP1' could not be located in BuildWallboard'.
MSB3155: Item 'Microsoft.Windows.Installer.3.1' could not be located in BuildWallboard'.
This is because on the build server needs a ‘default installation’ of Visual Studio Developer (or Suite). The publish function, like the MSTest function is not something the Team Build server can do bit itself it needs Visual Studio to do the heavy lifting.
I would say at this time unless you need 64Bit specific assemblies built you are best staying on a 32bit operating system. This will happily build MSIL .NET assemblies which I guess for most of us is the bulk of our work. OK you loose a bit of performance if you have 64bit hardware (or virtual hardware in our case), but I doubt this will be critical, shaving a few seconds of an automated build is not normally important.
My main reason for saying this is what as you extend your build process you will no doubt started to use community developed build activities, some of these seem to get a bit confused if you are on a 64bit OS. The issue seems to be that they cannot easily find the TFS Client assemblies in C:\Program File (x86) as opposed to C:\Program File. For example we have a build that automatically updates version numbers and deploys via ClickOnce; it works fine on a 32bit W2k8 build server, but on an identically configured 64bt W2K8 build server it gives the error:
error MSB4018: The "MSBuild.Community.Tasks.Tfs.TfsVersion" task failed unexpectedly.
error MSB4018: System.IO.FileNotFoundException: Could not load file or assembly 'file:///C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\Microsoft.TeamFoundation.Client.dll' or one of its dependencies. The system cannot find the file specified.
So what appears to be just a simple path issue, that is probably fixable in an XML configuration file or with search paths – but is it worth the effort? I would say not in general. I want to keep my installation as near default as possible, which I can with 32bit.
I have been rebuilding our TFS build systems on Hyper-V based virtualised hardware. The long term plan being to hold a configured build server as as Hyper-V template to we could prevision extra ones quickly, or rebuild all of them if we need to upgrade some library or tool; in effect to give us revision control over our build servers.
All seemed to be going OK, initially existing builds seemed to be running OK when targeted at the new server. However I soon saw that tests were failing with the error
MSBUILD : warning MSB6006: "MSTest.exe" exited with code 1
Further digging into the build log showed the tests were being run but the copy to the drop location was failing.
Side note: if you read older TFS documentations and many blogs it says to add the flag
/v:diagnostic
to the TFSbuild.rsp file to get more logging – this is wrong with MSBuild 3.5 as used by TFS 2008. This now defaults to the highest level of logging, so to reduced it you must use
/fileLoggerParameters:verbosity=normal
so no help in debugging. Anyway back to the plot……
In the past our single build server used a share on its own disk as the file drop, but now as we intend multiple build servers I decided to have a central build share on main data store server. This had been setup with read/write access to the folder and the share associated for the tfsbuild domain user that the Team Build service runs as.
Turns out this is not enough. You also have to give read/write access to the tfsservice domain user as well. It seems the publish of the test results comes from the TFS server not the build process. hence needing the extra rights. Once the change is made all work fine
We use the SharePoint Visual Studio Project Template on CodePlex to create WSP deployment packages for our SharePoint features. I tend to think of this WSP creation project in the same way as a MSI installer; so we don’t put SharePoint components into the WSP solution itself, it is an extra project in the solution that assembles the components from a variety of other solutions (e.g. web parts, workflows, event receivers, shared libraries for the GAC etc) and builds a single deployable WSP file.
Running locally on a developers PC inside Visual Studio this template has worked well, the only change I make from the default is to alter the WSP projects pre-build event script to xcopy all the files into the correct directories to allow the VBScript files to create the WSP.
In our drive to automation and automatic testing I have been looking at getting the WSP created as part of our TFS Team Build process. It turns out you get a few problems because Visual Studio and Team Build do macro expansion differently.
So my Pre-build event becomes
echo PREBUILD STARTED
rem Check if we running in VS or Teambuild
if not exist "..\..\..\CLIENTLIBRARY\SharedLibProject\bin\$(ConfigurationName)\SharedLibProject.dll" goto tfsbuild
echo Copy from VS locations, in this sample we assume a shared library, a webpart and some javascript
xcopy "..\..\..\CLIENTLIBRARY\SharedLibProject\bin\$(ConfigurationName)\SharedLibProject.dll" "$(ProjectDir)DLLS\GAC\" /F /R /Y
xcopy "..\..\..\Web Part\bin\$(ConfigurationName)\*.dll" "$(ProjectDir)DLLS\GAC\" /F /R /Y
xcopy "$(SolutionDir)HOST\bin\HOST.dll" "$(ProjectDir)DLLS\GAC\" /F /R /Y
xcopy "$(SolutionDir)HOST\json*" "$(ProjectDir)TEMPLATE\LAYOUTS" /F /R /Y
xcopy "$(SolutionDir)HOST\*.js" "$(ProjectDir)TEMPLATE\LAYOUTS" /F /R /Y
goto end
:tfsbuild
echo Copy from TFS build locations
xcopy "$(outdir)\SharedLibproject.dll" "$(ProjectDir)DLLS\GAC\" /F /R /Y
xcopy "$(outdir)\WebPart.Core.dll" "$(ProjectDir)DLLS\GAC\" /F /R /Y
xcopy "$(outdir)\WebPart.UI.dll" "$(ProjectDir)DLLS\GAC\" /F /R /Y
xcopy "$(outdir)\Host.dll" "$(ProjectDir)DLLS\GAC\" /F /R /Y
xcopy "$(SolutionDir)HOST\json*" "$(ProjectDir)TEMPLATE\LAYOUTS\json*" /F /R /Y
xcopy "$(SolutionDir)HOST\*.js" "$(ProjectDir)TEMPLATE\LAYOUTS\*.js" /F /R /Y
:end
echo PREBUILD COMPLETE
Key points to note here are
- For Visual Studio you can use Xcopy /s it makes no difference as there are no sub-directories (so you might ask why use it it all, I guess in some cases a generic copy all is easier than specifying a fixed file and directory). This is not the case for Team Build, if you use /s you can get multiple copies of DLLs in sub-directories created. This is because of the way Team Build structures it’s directories. The $(outdir) is not a subdirectory of the $(solutiondir) as it is in Visual Studio, it is an absolute path defined for the build agents settings where all the outputs for all the projects in the build are assembled. So, depending on the project type, you seem to get sub directories. So it is best to be very specific as to what to copy, avoid wildcards and recursion.
- When doing a wildcard xcopy as with json* files on Team Build you must specify the copy to file name i.e. json*, if you don’t you get the question ‘is the target a file or a directory’ message which obviously kills the build. This does not occur within Visual Studio.
It is also worth altering the post build event, by default the WSP is created in the project root, but if it is copied to the $(outdir) it ends up in the Team build drop location, so can be picked up by anyone, just like a DLL.
echo POSTBUILD STARTED
rem commented out as the build box does not have SharePoint installed
rem this could be wrappered in the chheck to see if the directory is present if we are on tema build or not
rem XCOPY "$(ProjectDir)TEMPLATE\*" "C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE" /S /F /R /Y
echo Run the VBscripts to create the XML files
"$(ProjectDir)CreateManifest.vbs" "$(ProjectDir)" "$(ProjectName)"
"$(ProjectDir)CreateCabDDF.vbs" "$(ProjectDir)" "$(ProjectName)"
echo Build the WSP
cd "$(ProjectDir)"
makecab.exe /F cab.ddf
echo Copy it to the out directory
xcopy *.wsp "$(TargetDir)\*.wsp" /y
echo POSTBUILD COMPLETE
However your problems do not end here. If you build this WSP project locally on a development PC all is fine. However (depending upon you project) it may fail on Team Build, well actually not fail just pause forever. This due to the way that Team Build checks out folders. The WSP project has a folder structure you drop files in that the VBScript files scan to create the manifest and then the WSP. If one of these directories is empty then it is not created on the build box and the VBScript stalls.
The solution is simply just add an extra folder exists check in the CreateCabDDF.vbs file’s EnumFolder method
sub EnumFolder(sFolder, sRelativePath)
dim oFolder, oFolders, oSub, oFile
rem this is the extra line
If oFS.FolderExists(sFolder) Then
set oFolder = oFS.GetFolder(sFolder)
if (sRelativePath = "TEMPLATE") then sRelativePath = ""
if (sRelativePath = "FEATURES") then sRelativePath = ""
if (sRelativePath <> "") then sRelativePath = sRelativePath + "\"
for each oFile in oFolder.Files
oDDF.WriteLine """" + oFile.Path + """" + vbTab + """" + sRelativePath + oFile.Name + """"
next
if (sRelativePath <> "" and InStr(1, sFolder, "FEATURES") > 0) then
sRelativePath = "FEATURES\" + sRelativePath
end if
for each oSub in oFolder.SubFolders
EnumFolder oSub.Path, sRelativePath + oSub.Name
next
end if
end sub
Once this is all one the you can build the project in the Team Build
At the last MVP Summit Steve Ballmer said “I’m going to ask you one week switch your default [search engine], one week. At the end of the week…I’ll want feedback, how was your week, what happened, what did you like, what didn’t you like … Can I make that deal with you? (Cheers and applause.) That’s the deal.”
Well the week was last week, and how did I find Live Search?
I have to say it is vastly improved, in the past I just assumed Live Search would find nothing of use, especially if I was after something I would expect to find on a Microsoft site like TechNet.
This week I have found that though it does not return exactly the same a Google, it is just as useful; in fact the two are fairly complimentary. For most searches it does not now seem to matter which one I used, but when really digging one might turn up something the other does not.
So am I going to move back to Goggle? Well I am just not sure it matters for day to day searching. I certainly don’t now feel the need to change my default search engine to Google immediately when I setup a PC as I used to.

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 = TestHelpers.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>(Members.ReturnRecursiveFakes);
Isolate.Swap.NextInstance<SPSite>().With(fakeSite);
Isolate.WhenCalled(() => fakeSite.RootWeb.Lists[ListName].Items).WillReturnCollectionValuesOf(
new List<SPItem> {
Isolate.Fake.Instance<SPItem>(),
Isolate.Fake.Instance<SPItem>(),
Isolate.Fake.Instance<SPItem>() });
for (int i = 0; i < 3; i++)
{
Isolate.WhenCalled(() => fakeSite.RootWeb.Lists[ListName].Items["Title"]).WillReturn(string.Format("Title {0}", i));
Isolate.WhenCalled(() => fakeSite.RootWeb.Lists[ListName].Items["Email Address"]).WillReturn(string.Format("email{0}@email.com", i));
}
}
}
}
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.
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
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 created inside the aspx page so we don't do it here
// create the mock SP Site we are using
SPSite fakeSite = Isolate.Fake.Instance<SPSite>(Members.ReturnRecursiveFakes);
Isolate.Swap.NextInstance<SPSite>().With(fakeSite);
Isolate.WhenCalled(() => fakeSite.RootWeb.Lists[ListName].Items).WillReturnCollectionValuesOf(
new List<SPItem> {
Isolate.Fake.Instance<SPItem>(),
Isolate.Fake.Instance<SPItem>(),
Isolate.Fake.Instance<SPItem>() });
for (int i = 0; i < 3; i++)
{
Isolate.WhenCalled(() => fakeSite.RootWeb.Lists[ListName].Items["Title"]).WillReturn(string.Format("Title {0}", i));
Isolate.WhenCalled(() => fakeSite.RootWeb.Lists[ListName].Items["Email Address"]).WillReturn(string.Format("email{0}@email.com", i));
}
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);
}
}
}
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 dont 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.
Applications to attend Software Craftsmanship 2009 have opened, this is a free conference that aims to discuss ‘the "hard skills" that programmers and teams require to deliver high quality working software’.
If you have not heard of Software Craftsmanship take a look at Peter McBreen’s book Software Craftsmanship: The New Imperative well worth the read.
