SharePoint 2007 : Utilisation du pattern MVP et Tests Unitaires avec TypeMock

Comme nous avons pu le voir dans le dernier post à ce sujet, après avoir fait l’accès aux données pour la fonctionnalité Quizz, il est maintenant nécessaire de développer “l’intelligence” de la webpart.

Dans notre cas, cela se résume à 3 fonctionnalités :

  1. l’initialisation de la webpart
  2. La validation des données saisies par l’utilisateur
  3. Le passage à la prochaine question

Mais avant de commencer, il est nécessaire de comprendre comment cette webpart est “architecturée”. En effet, dans le but de faciliter au maximum l’utilisation de test unitaires et le découplage de l’application, j’ai choisi d’utiliser le pattern MVP (Model, View, Présenter) qui est en quelque sorte l’ancêtre du pattern MVC (Model , View, Controller).

Évidemment, ce travail est fortement basé sur le travail réalisé en amont par l’équipe de Patterns and Practices sur SharePoint que vous pourrez retrouvez sur codeplex : http://spg.codeplex.com ou sur le blog de Erwin van der Valk

image

Vous verrez qu’il existe déjà beaucoup d’informations liées à ce pattern sur la toile, je les ai regroupées sur mon compte delicious, que vous pourrez retrouver à cette url : http://delicious.com/philippesentenac/mvp

Pour faire simple, nous allons développé une webpart qui va charger la View (un user control), cette vue (qui n’est là que pour le design de l’application) va instancier son présenter qui s’occupera de l’accès au données et de tout les traitement nécessaire (validation, erreur, etc.) et qui communiquera avec la vue via une interface.

Mais assez de discours, voyons tout cela dans le détail :

   1:  using TechDays.Demo.SharePoint.BusinessEntities;
   2:   
   3:  namespace TechDays.Demo.SharePoint.Presenter
   4:  {
   5:      /// <summary>
   6:      /// Interface for the Quizz WebPart
   7:      /// </summary>
   8:      public interface IQuizzView
   9:      {
  10:          /// <summary>
  11:          /// Gets or sets the name of the list.
  12:          /// </summary>
  13:          /// <value>The name of the list.</value>
  14:          string ListName { get; set; }
  15:   
  16:          /// <summary>
  17:          /// Gets or sets the question.
  18:          /// </summary>
  19:          /// <value>The question.</value>
  20:          Question Question { get; set; }
  21:   
  22:          /// <summary>
  23:          /// Gets or sets the error message.
  24:          /// </summary>
  25:          /// <value>The error message.</value>
  26:          string ErrorMessage { get; set; }
  27:      }
  28:  }

Ici, nous avons donc une interface très simple qui nous permettra de communiquer avec le presenter notamment concernant la liste sur laquelle l’utilisateur souhaite effectué le quizz, la question actuellement affichée par la webpart et eventuellement un message d’erreur.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using SharePointOfView.Patterns.MVP;
   5:  using SharePointOfView.Utilities;
   6:  using TechDays.Demo.SharePoint.BusinessEntities;
   7:  using TechDays.Demo.SharePoint.Model;
   8:   
   9:  namespace TechDays.Demo.SharePoint.Presenter
  10:  {
  11:      /// <summary>
  12:      /// The Presenter for the Quizz WebPart
  13:      /// </summary>
  14:      [CLSCompliant(false)]
  15:      public class QuizzPresenter : Presenter<IQuizzView>
  16:      {
  17:          /// <summary>
  18:          /// Called when the view is initialized.
  19:          /// </summary>
  20:          public override void OnViewInitialized()
  21:          {
  22:              if (View.Question == null)
  23:              {
  24:                  try
  25:                  {
  26:                      var quizzRep = new QuestionRepository(this.SPContext.Web, this.View.ListName);
  27:                      Question question = quizzRep.GetNextQuestion(0);
  28:                      View.Question = question;
  29:                  }
  30:                  catch (Exception ex)
  31:                  {
  32:                      string errorMessage = Localization.GetResource("Quizz_Presenter_ErrorMessage", "Quizz.Presenter", this.SPContext.Web.Language);
  33:                      View.ErrorMessage = errorMessage + ex.Message;
  34:                  }
  35:              }
  36:          }
  37:   
  38:          /// <summary>
  39:          /// Validates the specified answers.
  40:          /// </summary>
  41:          /// <param name="answers">The answers.</param>
  42:          /// <param name="explanation">The explanation.</param>
  43:          /// <returns>True is the answers are correct, else False</returns>
  44:          public bool Validate(Dictionary<int, bool> answers, out string explanation)
  45:          {
  46:              StringBuilder explanationBuilder = new StringBuilder();
  47:              explanation = string.Empty;
  48:   
  49:              if (View.Question == null)
  50:              {
  51:                  View.ErrorMessage = Localization.GetResource("Quizz_Presenter_ErrorMessage", "Quizz.Presenter", this.SPContext.Web.Language);
  52:                 return false;
  53:              }
  54:   
  55:              bool correct = true;
  56:              Question currentQuestion = View.Question;
  57:   
  58:              foreach (KeyValuePair<int, bool> answer in answers)
  59:              {
  60:                  if (currentQuestion.Answers[answer.Key] == null || currentQuestion.Answers[answer.Key].IsValid != answer.Value)
  61:                  {
  62:                      correct = false;
  63:                  }
  64:              }
  65:   
  66:              string labelCorrect = Localization.GetResource("Quizz_Presenter_Correct", "Quizz.Presenter", this.SPContext.Web.Language);
  67:              string labelIncorrect = Localization.GetResource("Quizz_Presenter_Incorrect", "Quizz.Presenter", this.SPContext.Web.Language);
  68:              string formattedExplanation = Localization.GetResource("Quizz_Presenter_ExplanationFormat", "Quizz.Presenter", this.SPContext.Web.Language);
  69:   
  70:              explanationBuilder.AppendFormat(formattedExplanation, correct ? labelCorrect : labelIncorrect);
  71:              currentQuestion.Answers.FindAll(answ => answ.IsValid == true).ForEach(delegate(Answer answ) { explanationBuilder.Append(answ.Content + " "); });
  72:              explanation = explanationBuilder.ToString().TrimEnd();
  73:   
  74:              return correct;
  75:          }
  76:   
  77:          /// <summary>
  78:          /// Sets the next question.
  79:          /// </summary>
  80:          /// <param name="questionId">The question id.</param>
  81:          public void SetNextQuestion(int questionId)
  82:          {
  83:              var quizzRep = new QuestionRepository(this.SPContext.Web, this.View.ListName);
  84:              Question nextQuestion = quizzRep.GetNextQuestion(questionId);
  85:              View.Question = nextQuestion;
  86:          }
  87:      }
  88:  }
Ici on retrouve donc les 3 actions que le presenter doit traiter, nous allons maintenant les détailler en commentant un peu plus le code :
  1. l’initialisation de la webpart : Si la vue n’as pas déjà été initialisée, on instancie notre QuestionRepository (vu dans le précédent post), on récupère la première question de la liste et on assigne cette question à la vue. En cas d’erreur, on affiche un message adéquat.
  2. La validation des données saisies par l’utilisateur : On récupère la question provenant de la vue et la/les réponse(s) de l’utilisateur et on vérifie si ces réponses sont correctes. En fonction on retourne un booléen et un chaine de caractères informant l’utilisateur de son résultat.
  3. Le passage à la prochaine question : On instancie simplement notre repository et on assigne la vue à la question suivante.

Pour ceux qui se poseraient la question, voilà comment est définie une question :

   1:  using System;
   2:  using System.Collections.Generic;
   3:   
   4:  namespace TechDays.Demo.SharePoint.BusinessEntities
   5:  {
   6:      /// <summary>
   7:      /// Used to store all the informations about a Quizz Question
   8:      /// </summary>
   9:      [Serializable()]
  10:      public class Question
  11:      {
  12:          /// <summary>
  13:          /// Gets or sets the ID.
  14:          /// </summary>
  15:          /// <value>The Question's ID</value>
  16:          public int Id { get; set; }
  17:   
  18:          /// <summary>
  19:          /// Gets or sets the content.
  20:          /// </summary>
  21:          /// <value>The content.</value>
  22:          public string Content { get; set; }
  23:   
  24:          /// <summary>
  25:          /// Gets or sets the category.
  26:          /// </summary>
  27:          /// <value>The category.</value>
  28:          public Category Category { get; set; }
  29:   
  30:          /// <summary>
  31:          /// Gets or sets the answers.
  32:          /// </summary>
  33:          /// <value>The answers.</value>
  34:          public List<Answer> Answers { get; set; }
  35:      }
  36:   
  37:      /// <summary>
  38:      /// Used to know any choices for a question and if they are valid
  39:      /// </summary>
  40:      [Serializable()]
  41:      public class Answer
  42:      {
  43:          /// <summary>
  44:          /// Gets or sets the content.
  45:          /// </summary>
  46:          /// <value>The content.</value>
  47:          public string Content { get; set; }
  48:   
  49:          /// <summary>
  50:          /// Gets or sets a value indicating whether this instance is valid.
  51:          /// </summary>
  52:          /// <value><c>true</c> if this instance is valid; otherwise, <c>false</c>.</value>
  53:          public bool IsValid { get; set; }
  54:      }
  55:   
  56:      /// <summary>
  57:      /// Used to store the information about a Quizz Category
  58:      /// </summary>
  59:      [Serializable()]
  60:      public class Category
  61:      {
  62:          /// <summary>
  63:          /// Gets or sets the title.
  64:          /// </summary>
  65:          /// <value>The title.</value>
  66:          public string Title { get; set; }
  67:   
  68:          /// <summary>
  69:          /// Gets or sets the ID.
  70:          /// </summary>
  71:          /// <value>The Category's ID</value>
  72:          public int Id { get; set; }
  73:      }
  74:  }

Le plus dur est fait a priori, maintenant j’aimerai pouvoir m’assurer que tout celà fonctionne bien. Je vais donc coder quelques tests unitaires notamment avec l’utilisation de TypeMock.

Pour bien comprendre l’intérêt des tests qui vont suivre, il faut bien voir que dans le précédent post, j’ai valider que mon accès au données fonctionnait correctement. Dorénavant lorsque je ferais appel à mon QuestionRepository dans mes test unitaires, je pourrais “mocker” cet accès afin de rendre mes tests isolés de l’envionnement SharePoint.

   1:  using System.Collections.Generic;
   2:  using Microsoft.SharePoint;
   3:  using Microsoft.VisualStudio.TestTools.UnitTesting;
   4:  using TechDays.Demo.SharePoint.BusinessEntities;
   5:  using TechDays.Demo.SharePoint.Model;
   6:  using TechDays.Demo.SharePoint.Presenter;
   7:  using TypeMock.ArrangeActAssert;
   8:   
   9:  namespace TechDays.Demo.SharePoint.Presenter.Tests
  10:  {
  11:      /// <summary>
  12:      /// Test Class that will handle all the Tests for the Quizz Presenter
  13:      /// </summary>
  14:      [TestClass]
  15:      public class QuizzPresenterTests
  16:      {
  17:          #region Fields
  18:          ...
  19:          #endregion
  20:   
  21:          /// <summary>
  22:          /// Initializes a new instance of the QuizzPresenterTests class.
  23:          /// </summary>
  24:          public QuizzPresenterTests()
  25:          {
  26:              IQuizzView view = Isolate.Fake.Instance<IQuizzView>(Members.ReturnNulls);
  27:              SPContext context = Isolate.Fake.Instance<SPContext>(Members.ReturnRecursiveFakes);
  28:              this.presenter = new QuizzPresenter();
  29:              this.presenter.View = view;
  30:              this.presenter.SPContext = context;
  31:   
  32:              this.questionRepository = Isolate.Fake.Instance<QuestionRepository>();
  33:   
  34:              #region Init Questions q1 & q2
  35:              ...
  36:              #endregion
  37:   
  38:              this.questions = new List<Question> { q1, q2 };
  39:          }
  40:   
  41:          /// <summary>
  42:          /// When onViewInitialized is called, it should set the view at the first Quizz Animaux's question.
  43:          /// </summary>
  44:          [TestMethod]
  45:          [Isolated]
  46:          public void OnViewInitializedShouldSetViewAtFirstQuestionOfQuizzAnimaux()
  47:          {
  48:              Isolate.Swap.NextInstance<QuestionRepository>().With(this.questionRepository);
  49:              Isolate.WhenCalled(() => this.questionRepository.GetNextQuestion(0)).WillReturn(this.questions[0]);
  50:   
  51:              this.presenter.OnViewInitialized();
  52:   
  53:              Assert.IsNotNull(this.presenter.View.Question);
  54:              Assert.AreEqual(this.questions[0].Content, this.presenter.View.Question.Content);
  55:          }
  56:      }
  57:  }
Regardons ce qu’il se passe pendant le test de la première méthode : OnViewInitializedShouldSetViewAtFirstQuestionOfQuizzAnimaux.
  • ligne 48, Grâce à TypeMock, je précise que la prochaine fois que l’objet QuestionRepository est appelé dans la suite de mon code, j’utiliserai ma propre instance.
  • ligne 49, je définie explicitement que l’appel à la méthode GetNextQuestion(0) retournera la question q1.
  • ligne 50, j’appelle “normalement” la méthode
  • ligne 51, je vérifie que le traitement a bien été effectué.

L’avantage d’utiliser Typemock dans ce cas précis est de nous permettre de nous dissocier complètement de SharePoint et de son SPContext en créant des Fake. Notamment gràce à cette ligne de code  :

SPContext context = Isolate.Fake.Instance<SPContext>(Members.ReturnRecursiveFakes));

Ainsi, si on prend un méthode un peu plus “complexe” comme la validation. On peut réaliser ce genre de test :

   1:  /// <summary>
   2:  /// When Validate is called, it should return false and an "Incorrect" explanation.
   3:  /// </summary>
   4:  [TestMethod]
   5:  [Isolated]
   6:  public void ValidateShouldReturnFalseAndIncorrectExplanation()
   7:  {
   8:      // Arrange
   9:      Isolate.Swap.NextInstance<QuestionRepository>().With(this.questionRepository);
  10:      Isolate.WhenCalled(() => this.questionRepository.GetQuestion(1)).WillReturn(this.questions[0]);
  11:   
  12:      Dictionary<int, bool> userAnswers = new Dictionary<int, bool>();
  13:      userAnswers.Add(0, true);
  14:      userAnswers.Add(1, true);
  15:      userAnswers.Add(2, false);
  16:      string explanation = string.Empty;
  17:      this.presenter.View.Question = this.questions[0];
  18:   
  19:      // Act
  20:      bool expected = this.presenter.Validate(userAnswers, out explanation);
  21:   
  22:      // Assert
  23:      Assert.IsFalse(expected);
  24:      Assert.AreEqual("Incorrect ! la réponse est : Egypte", explanation);
  25:  }

Ici, je simule l’interaction utilisateur en générant moi-même les réponses (userAnswers), récupère la question q1 et Fake le contexte SharePoint.

Par la suite, je fais appel à la méthode et verifie que le résultat obtenu correspond au resultat attendu.

image

Au final, nous avons réussi à obtenir nos tests unitaires de notre WebPart dans notre environnement Visual Studio avec une minimum de dépendance à SharePoint.

La suite du pattern MVP au prochain post :)

PS : Vous pourrez retrouvez tout celà dans la présentation que nous avons donné au Techdays 2009 avec Adrien. [Microsoft TechDays 2009] - Industrialisation du développement SharePoint avec Team System - 1/3

<Philippe/>

Publié mardi 5 mai 2009 12:22 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

# re: SharePoint 2007 : Utilisation du pattern MVP et Tests Unitaires avec TypeMock @ mercredi 6 mai 2009 13:06

Introduire du MVP et des tests unitaires sur Sharepoint comme tu l'as fait est vraiment top. Comme quoi les bonnes pratiques ne sont pas du tout liées à un contexte ou une techno. Pour aller plus loin tu pourrais peut etre revenir sur le principe du Mock, ce que cela fait et ce que cela t'apporte par rapport à ton contexte (et aussi préciser que typemock est payant mais qu'il y a d'autres solutions alternatives).

Rui

# re: SharePoint 2007 : Utilisation du pattern MVP et Tests Unitaires avec TypeMock @ mercredi 6 mai 2009 13:32

Thanks !

j'avais parlé de TypeMock a l'époque dans un post précédent mais c'est vrai que j'ai toujours tendance à oublier ce genre de "détails" :)

Ce sont de bonnes suggestions, je vais travailler dessus !

phil

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

- 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

- SharePoint Online: Script PowerShell pour supprimer une colonne dans tous les sites d’une collection par Blog Technique de Romelard Fabrice le 11-27-2018, 18:01