But it works on my PC!

The random thoughts of Richard Fennell on technology and software development

Running TypeMock based test in Team Build

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

My problems with Live Messenger inside Visual Studio are fixed.

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

clip_image002

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

Update on using StyleCop in TFS Team Build

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

image

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.

MSB3155 errors in Team build when publishing to click once

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.

Should my TFS Build Server be 32bit or 64bit?

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.

Getting MSB6006 errors for MSTest under TFS Team Build 2008

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

TFS TeamBuild and Sharepoint WSP deployment (and any post build events for that matter)

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

Steve Ballmer’s MVP Live Search Challenge

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.

live

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.