SharePoint 2007 : Unit Test, TypeMock et SharePoint

Après un petit mois de repos et une présentation sur SharePoint et Team System qui s'est très bien passée la semaine dernière, je me suis dit qu'il était temps de recommencer à bloguer :)

Et du coup, je voulais vous parler de mon sujet du moment : Les tests unitaires avec SharePoint.

Avant de commencer, je vous donne un peu le contexte : Mon chef de projet m'a demandé de lui développer une webpart qui affichera toutes les bibliothèques et liste ayant des permissions personnalisés.

Ma réponse : Développement d'une webpart basée sur la XSLTransformWebpart de SharePointofView.

Ainsi je m'évite toute la plomberie et je me concentre sur le bout de code me permettant de lister les bibliothèques et listes ayant des permissions personnalisés.

Voyons ce que ca donne :

UnitTestSharePointProject

feature.xml :

<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
         Id="93C817FC-7C45-4A54-8A47-11605E5E2D55"
         Title="$Resources:FeatureTitle;"
         Description="$Resources:FeatureDescription;"
         Scope="Site"
         Hidden="False"
         Version="1.0.0.0"
         DefaultResourceFile="_Res"
         ReceiverAssembly="SharePointProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d683633d8c7b70eb"
         ReceiverClass="SharePointProject.UniquePermissionsFeatureReceiver">
 
  <ElementManifests>
    <ElementManifest Location="element.xml" />
  </ElementManifests>
 
</Feature>

On remarquera l'utilisation du DefaultResourceFile pour utilisé les fichiers Ressources de la feature et l'utilisation d'un FeatureReceiver qui sera utilisé pour supprimer la webpart uploadé du catalog de webpart lors de la désactivation.

element.xml :

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Url="_catalogs/wp"
          Path=""
          RootWebOnly="TRUE">
    <File Url="UniquePermission.webpart"
          Type="GhostableInLibrary">
    </File>
  </Module>
</Elements>

UniquePermissions.webpart :

<?xml version="1.0" encoding="utf-8" ?>
<webParts>
  <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
    <metaData>
      <type name="SharePointProject.UniquePermissionsWebpart, SharePointProject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d683633d8c7b70eb" />
      <importErrorMessage>Error importing this Web Part!!</importErrorMessage>
    </metaData>
    <data>
      <properties>
        <property name="Title" type="string">Unique Permissions</property>
        <property name="Xsl" type="string">&lt;?xml version="1.0" encoding="utf-8" ?&gt;
          &lt;xsl:stylesheet version="1.0" xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
          &lt;xsl:template match="/Lists/List"&gt;
          &lt;p&gt;
          &lt;xsl:choose&gt;
          &lt;xsl:when test="@Template = 'DocumentLibrary'"&gt;
          &lt;img src="\_layouts\images\itdl.gif" alt="DocumentLibrary"/&gt;
          &lt;/xsl:when&gt;
          &lt;xsl:when test="@Template = 'PictureLibrary'"&gt;
          &lt;img src="\_layouts\images\itil.gif" alt="PictureLibrary"/&gt;
          &lt;/xsl:when&gt;
          &lt;xsl:when test="@Template = 'DicussionList'" &gt;
          &lt;img src="\_layouts\images\itdisc.gif" alt="DicussionList"/&gt;
          &lt;/xsl:when&gt;
          &lt;xsl:otherwise&gt;
          &lt;img src="\_layouts\images\itgen.gif" alt="GenericList"/&gt;
          &lt;/xsl:otherwise&gt;
          &lt;/xsl:choose&gt;
          &lt;b&gt;
          &lt;a href="{@Url}" style="padding:5px;"&gt;
          &lt;xsl:value-of select="@Name" /&gt;
          &lt;/a&gt;
          &lt;/b&gt;
          &lt;/p&gt;
          &lt;/xsl:template&gt;
          &lt;/xsl:stylesheet&gt;
        </property>
      </properties>
    </data>
  </webPart>
</webParts>

Resources.resx :

<data name="FeatureDescription" xml:space="preserve">
  <value>Add UniquePermission WebPart in the Site Collection WebPart Catalog, This WebPart show the lists with custom permission</value>
</data>
<data name="FeatureTitle" xml:space="preserve">
  <value>DEMO : UniquePermission</value>
</data>

Attention, je n'y ai mis que les informations concernant la feature, il est bien plus verbeux normalement.

UniquePermissionsFeatureReceiver.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using SharePointOfView.ExtensionMethods;
using Microsoft.SharePoint.WebPartPages;
using System.Web.UI.WebControls.WebParts;
 
namespace SharePointProject
{
    /// <summary>
    /// Feature Receiver for the UniquePermission Feature
    /// </summary>
    public class UniquePermissionsFeatureReceiver : SPFeatureReceiver
    {
        /// <summary>
        /// Occurs after a Feature is activated.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        /// <remarks>Not Implemented</remarks>
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
        }
 
        /// <summary>
        /// Occurs when a Feature is deactivated.
        /// Deletes all the files uploaded via the feature (ie: UniquePermissions.webpart
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            properties.SovDeleteUploadedFiles();
        }
 
        /// <summary>
        /// Occurs after a Feature is installed.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        /// <remarks>Not Implemented</remarks>
        public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {
        }
 
        /// <summary>
        /// Occurs when a Feature is uninstalled.
        /// </summary>
        /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
        /// <remarks>Not Implemented</remarks>
        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        {
        }
    }
}

Ici on utilise le plus simplement du monde l'extension de méthode de SharePointOfView : properties.SovDeleteUploadedFiles() qui supprime tout les fichiers uploadés via cette feature (ie : la webpart)

UniquePermissionsWebPart.cs

using System.Xml;
using Microsoft.SharePoint;
using SharePointOfView.WebControls;
 
namespace SharePointProject
{
    /// <summary>
    /// Shows all the list/doc lib with Unique Permissions
    /// </summary>
    public class UniquePermissionsWebpart : XslTransformWebPart
    {
        /// <summary>
        /// Gets the data.
        /// </summary>
        /// <returns>an XML string describing the data</returns>
        protected override string GetData()
        {
            return this.GetListsWithUniquePermissions(SPContext.Current.Web);
        }
 
        /// <summary>
        /// Gets the lists with unique permissions.
        /// </summary>
        /// <param name="web">The web where we look for custom permissions</param>
        /// <returns>an XML string describing the data</returns>
        protected string GetListsWithUniquePermissions(SPWeb web)
        {
            XmlDocument xmlDoc = new XmlDocument();
            XmlElement elementLists = xmlDoc.CreateElement("Lists");
            xmlDoc.AppendChild(elementLists);
 
            foreach (SPList list in web.Lists)
            {
                if (list.HasUniqueRoleAssignments && !list.Hidden)
                {
                    XmlElement elementList = xmlDoc.CreateElement("List");
                    elementList.SetAttribute("Id", list.ID.ToString());
                    elementList.SetAttribute("Name", list.Title);
                    elementList.SetAttribute("Url", list.DefaultViewUrl);
                    elementList.SetAttribute("Template", list.BaseTemplate.ToString());
 
                    elementLists.AppendChild(elementList);
                }
            }
            return xmlDoc.OuterXml;
        }
    }
}

Le code reste relativement simple en héritant de la webpart XSLTransformWebPart de SharePointOfView et se concentre uniquement sur le mécanisme permettant de lister toute les bibliothèques et liste qui nous intèressent. Idéalement j'aurais pu mettre ce bout de code dans une autre assembly mais vous voyez l'idée :)

Hop, on compile tout et génère le WSP (avec WSPBuilder) et ca fonctionne.

Mais pour revenir au titre du post, comment faire si je souhaite tester et valider le fonctionnement de ma fonction GetListWithUniquePermissions(SPWeb) sans forcément à devoir le faire visuellement en déploiement tout mon bazard dans SharePoint ?

Et bien tout simplement en crééant un nouveau projet de test à l'intérieur de ma solution et d'utiliser TypeMock ! Pourquoi TypeMock ? Tout simplement car c'est ,à ma connaissance, le seul framework de test unitaire qui nous permet d'utiliser simplement toute les classes internal et sealed du framework SharePoint.

UniquePermissionWebpartTests.cs :

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.SharePoint;
using System.Xml;
using System.Reflection;
using SharePointProject;
 
namespace SharePointProject.UnitTest
{
    /// <summary>
    /// Summary description for UniquePermissionWebpartTests
    /// </summary>
    [TestClass]
    public class UniquePermissionWebpartTests
    {
        private SPWeb _SPWeb;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="UniquePermissionWebpartTests"/> class.
        /// </summary>
        public UniquePermissionWebpartTests()
        {
            _SPWeb = SharePointMocked.createMockedSPWeb(10);
        }
 
        /// <summary>
        /// Tests the results count.
        /// </summary>
        [TestMethod()]
        public void TestResultsCount()
        {
            int expectedCount = 4;
            int resultCount;
 
            UniquePermissionsWebpart wp = new UniquePermissionsWebpart();
 
            MethodInfo mi = wp.GetType().GetMethod("GetListsWithUniquePermissions", BindingFlags.NonPublic|BindingFlags.Public|BindingFlags.Instance);
 
            string result = mi.Invoke(wp, new object[] { _SPWeb }) as string;
 
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(result);
            XmlNodeList nodes = xmlDoc.SelectNodes("/Lists/List");
            resultCount = nodes.Count;
 
            Assert.AreEqual(resultCount, expectedCount);
        }
 
        /// <summary>
        /// Tests the valid list names.
        /// </summary>
        [TestMethod()]
        public void TestValidListNames()
        {
            List<string> expectedListNames = new List<string>() { "ListTitle_ 0", "ListTitle_ 3", "ListTitle_ 6", "ListTitle_ 9" };
            List<string> resultListNames = new List<string>();
 
            UniquePermissionsWebpart wp = new UniquePermissionsWebpart();
 
            MethodInfo mi = wp.GetType().GetMethod("GetListsWithUniquePermissions", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
 
            string result = mi.Invoke(wp, new object[] { _SPWeb }) as string;
 
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(result);
            XmlNodeList nodes = xmlDoc.SelectNodes("/Lists/List");
 
            foreach (XmlNode xmlNode in nodes)
                resultListNames.Add(xmlNode.Attributes["Name"].Value);
 
            Assert.ReferenceEquals(resultListNames, expectedListNames);
        }
 
 
        /// <summary>
        /// Tests the invalid list names.
        /// </summary>
        [TestMethod()]
        public void TestInvalidListNames()
        {
            List<string> notExpectedListNames = new List<string>() { "ListTitle_ 1", "ListTitle_ 2", "ListTitle_ 5", "ListTitle_ 9" };
            List<string> resultListNames = new List<string>();
 
            UniquePermissionsWebpart wp = new UniquePermissionsWebpart();
 
            MethodInfo mi = wp.GetType().GetMethod("GetListsWithUniquePermissions", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
 
            string result = mi.Invoke(wp, new object[] { _SPWeb }) as string;
 
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(result);
            XmlNodeList nodes = xmlDoc.SelectNodes("/Lists/List");
 
            foreach (XmlNode xmlNode in nodes)
                resultListNames.Add(xmlNode.Attributes["Name"].Value);
 
            Assert.AreNotEqual<List<string>>(notExpectedListNames,resultListNames);
        }
    }
}

Il convient d'expliquer plusieurs choses ici :

  • L'appel à la méthode qui m'intèresse de fait par réflection car elle est inaccessible autrement, comme je le disais tout à l'heure idéalement j'aurais pu la séparer et la mettre dans une autre assembly dite "Metier" et y acceder plus simplement... passons, c'est aussi l'ocasion de montrer que ce n'est pas rédibitoire.
  • Finalement, l'appel dans le constructeur à SharePointMocked.createMockedSPWeb(10) est la clef de voute permettant de tester mon code, le reste n'est que du test unitaire simple.

Voyons donc un peu comment ca fonctionne :

SharePointMocked.cs : basé sur un post de Michael Christensen

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using TypeMock;
using System.Collections;
 
namespace SharePointProject.UnitTest
{
    /// <summary>
    /// 
    /// </summary>
    public static class SharePointMocked
    {
        internal static SPWeb createMockedSPWeb(int numberOfLists)
        {
            Mock<SPWeb> web = MockManager.MockObject<SPWeb>(Constructor.Mocked);
            Guid newGuid = new Guid("F58C5BD1-9260-44eb-8F62-C3D22866894C");
            web.ExpectGetAlways("ID", newGuid);
            web.ExpectGetAlways("Title", "Title_" + newGuid);
            web.ExpectGetAlways("Url", "http://weburl/" + newGuid);
 
            List<SPList> lists = new List<SPList>();
            for (int i = 0; i < numberOfLists; i++)
            {
                // 4 Listes auront HasUniqueRoleAssignments == True
                bool hasUniqueRoleAssignements = i % 3 == 0;
                SPList list = createMockedSPList(hasUniqueRoleAssignements, i);
                lists.Add(list);
            }
 
            web.ExpectGetAlways("Lists", createMockedSPListCollection(lists.GetEnumerator()));
            return web.MockedInstance;
        }
 
 
        internal static SPList createMockedSPList(bool hasUniqueRoleAssignments, int index)
        {
            Mock<SPList> list = MockManager.MockObject<SPList>(Constructor.Mocked);
            list.ExpectGetAlways("ID", new Guid("8f670e13-8718-41db-a4a9-25d0ae5c60a6"));
            list.ExpectGetAlways("Title", "ListTitle_ " + index);
            list.ExpectGetAlways("DefaultViewUrl", "http://listurl/" + index);
            list.ExpectGetAlways("BaseTemplate", index % 2 == 0 ? SPListTemplateType.GenericList : SPListTemplateType.DocumentLibrary);
            list.ExpectGetAlways("HasUniqueRoleAssignments", hasUniqueRoleAssignments);
            list.ExpectGetAlways("Hidden", false);
            return list.MockedInstance;
        }
 
        internal static SPListCollection createMockedSPListCollection(IEnumerator enumerator)
        {
            Mock<SPListCollection> listCollection = MockManager.MockObject<SPListCollection>(Constructor.Mocked);
            listCollection.ExpectAndReturn("System.Collections.IEnumerable.GetEnumerator", enumerator);
            return listCollection.MockedInstance;
        }
    }
}

Au final, il suffit de créer simplement un Mock de SPWeb qui retournera certaines valeurs spécifiques (ID, Title, Url) mais aussi une SPListCollection particulière avec 10 listes dont 1 sur 3 aura des permissions uniques et 1 sur 2 aura sera une librairie de document.

Une fois tout ce fichier de créé, on va pouvoir lancer nos tests avec succès :

UnitTestSharePoint

Donc pour faire fonctionner tout ça, vous allez avoir besoin de télécharger et installer :

SI vous voulez plus d'informations à ce sujet, vous pouvez consulter mon compte delicious : http://delicious.com/philippesentenac/sharepoint+typemock

Un petit bémol cependant concernant tout cela, dans un cas réel la création de ce MockedSPWeb sera bien plus complexe et prendrait trop de temps pour être réellement intéressant voire utile. La solution étant évidemment dans la création d'un outil de génération de Mock SharePoint automatique, ce qui fera évidemment le sujet de prochains posts :)

<Phil/>

Publié lundi 15 septembre 2008 14:33 par phil
Classé sous , , ,
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Commentaires

About phil

Philippe Sentenac est Consultant SharePoint à Wygwam en région Parisienne. Il intervient essentiellement sur des missions liées à SharePoint (2007 et 2010 ) mais aussi autour du Web 2.0. Plus généralement, il s'intéresse à l'ASP.Net (MVC) , à Silverlight, et à tout ce qui est orienté Web en rapport avec les nouvelles technologies, qu'il pratique depuis 2006. Féru de développement, il est passionné par les problématiques de méthodologies et d'industrialisation du développement.

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01