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 :
- l’initialisation de la webpart
- La validation des données saisies par l’utilisateur
- 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
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 :
- 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.
- 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.
- 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.
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/>