Pour reprendre l'exemple du Quizz là où je l'avais laissé il y a quelques semaines (cf le post de janvier ), il est désormais temps de se mettre à coder et à commencer par une des parties critiques de toutes applications : l'accès aux données.
Dans notre cas, l'accès au données se décompose en 3 parties : CategoryRepository, QuizzRepository, QuestionRepository ,
Prenon l'exemple de QuestionRepository est définit par l'interface suivante :
1: public interface IQuestionRepository
2: { 3: /// <summary>
4: /// Gets the next question.
5: /// </summary>
6: /// <param name="index">The index.</param>
7: /// <param name="category">The category.</param>
8: /// <returns>A Question</returns>
9: /// <exception cref="System.ArgumentException">Thrown when index is lower than 0</exception>
10: Question GetNextQuestion(int index, Category category);
11:
12: /// <summary>
13: /// Gets the next question.
14: /// </summary>
15: /// <param name="index">The index.</param>
16: /// <returns>A Question</returns>
17: /// <exception cref="System.ArgumentException">Throw when index is lower than 0</exception>
18: Question GetNextQuestion(int index);
19:
20: /// <summary>
21: /// Gets the question.
22: /// </summary>
23: /// <param name="index">The index.</param>
24: /// <returns>A Question</returns>
25: /// <exception cref="System.ArgumentException">Thrown when index is lower than 0</exception>
26: Question GetQuestion(int index);
27: }
A moi maintenant de développer l'accès aux données SharePoint en fonction de mes besoins (qui restent ici excessivement simples puisqu'il ne s'agit que d'un simple accès en lecture).
J'aurais pu décider d'implémenter l'accès au données via Linq4SP mais pour l'instant dans le cadre de ce post, je vais me restreindre à du bon vieux CAML et uniquement à l'implémentation de l'interface IQuestionRepository.
1: /// <summary>
2: /// This class is used to access the items inside a Quizz List at a specific Website
3: /// </summary>
4: [CLSCompliant(false)]
5: public class QuestionRepository : IQuestionRepository
6: { 7: #region Private Members
8: /// <summary>
9: /// The Quizz list where all the Queries will be processed
10: /// </summary>
11: private SPList quizzList;
12: #endregion
13:
14: #region Constructors
15:
16: /// <summary>
17: /// Initializes a new instance of the <see cref="QuestionRepository"/> class.
18: /// </summary>
19: /// <param name="web">The web used.</param>
20: /// <param name="listName">Name of the list.</param>
21: public QuestionRepository(SPWeb web, string listName)
22: { 23: if (web == null || string.IsNullOrEmpty(listName))
24: { 25: throw new ArgumentException("Parameters 'web' or 'listName' are required and shouldn't be null or empty"); 26: }
27:
28: if (!web.Lists.SovTryGet(listName, out this.quizzList))
29: { 30: throw new ArgumentOutOfRangeException(
31: String.Format(
32: CultureInfo.InvariantCulture,
33: "The list {0} doesn't exist inside the website '{1}'", 34: listName,
35: web.Title));
36: }
37: }
38: #endregion
39:
40: #region Public Methods
41: /// <summary>
42: /// Gets the next question.
43: /// </summary>
44: /// <param name="index">The index.</param>
45: /// <param name="category">The category.</param>
46: /// <returns>A Question</returns>
47: /// <exception cref="System.ArgumentException">Thrown when index is lower than 0</exception>
48: public Question GetNextQuestion(int index, Category category)
49: { 50: if (index < 0)
51: { 52: throw new ArgumentException("GetNextQuestion : index is lower than 0"); 53: }
54:
55: Question question = null;
56:
57: StringBuilder queryQuizzBuilder = new StringBuilder();
58:
59: queryQuizzBuilder.Append(" <OrderBy>"); 60: queryQuizzBuilder.Append(" <FieldRef Name=\"ID\" />"); 61: queryQuizzBuilder.Append(" </OrderBy>"); 62: queryQuizzBuilder.Append(" <Where>"); 63:
64: if (category != null && category.Id > 0)
65: { 66: queryQuizzBuilder.Append(" <And>"); 67: queryQuizzBuilder.Append(" <Eq>"); 68: queryQuizzBuilder.Append(" <FieldRef Name=\"QuizzCategory\" LookupId=\"TRUE\" />"); 69: queryQuizzBuilder.AppendFormat(" <Value Type=\"Lookup\">{0}</Value>", category.Id); 70: queryQuizzBuilder.Append(" </Eq>"); 71: }
72:
73: queryQuizzBuilder.Append(" <Gt>"); 74: queryQuizzBuilder.Append(" <FieldRef Name=\"ID\" />"); 75: queryQuizzBuilder.AppendFormat(" <Value Type=\"Counter\">{0}</Value>", index); 76: queryQuizzBuilder.Append(" </Gt>"); 77:
78: if (category != null && category.Id > 0)
79: { 80: queryQuizzBuilder.Append(" </And>"); 81: }
82:
83: queryQuizzBuilder.Append(" </Where>"); 84:
85: string queryQuizzCaml = queryQuizzBuilder.ToString();
86:
87: SPQuery queryQuizz = new SPQuery();
88: queryQuizz.Query = queryQuizzCaml;
89: queryQuizz.RowLimit = 1;
90:
91: SPListItemCollection resultQuestion = this.quizzList.GetItems(queryQuizz);
92:
93: if (resultQuestion.Count > 0)
94: { 95: SPListItem itemQuizz = resultQuestion[0];
96:
97: question = new Question
98: { 99: Id = (int)itemQuizz[SPBuiltInFieldId.ID],
100: Content = itemQuizz[Fields.QuizzQuestion] as string,
101: Answers = this.GetAnswersForQuestion(itemQuizz),
102: Category = this.GetCategoryForQuestion(itemQuizz)
103: };
104: }
105:
106: return question;
107: }
108:
109: /// <summary>
110: /// Gets the next question.
111: /// </summary>
112: /// <param name="index">The index.</param>
113: /// <returns>A Question</returns>
114: /// <exception cref="System.ArgumentException">Throw when index is lower than 0</exception>
115: public Question GetNextQuestion(int index)
116: { 117: return this.GetNextQuestion(index, null);
118: }
119:
120: /// <summary>
121: /// Gets the question.
122: /// </summary>
123: /// <param name="index">The index.</param>
124: /// <returns>A Question</returns>
125: /// <exception cref="System.ArgumentException">Thrown when index is lower than 0</exception>
126: public Question GetQuestion(int index)
127: { 128: if (index < 0)
129: { 130: throw new ArgumentException("GetQuestion : index is lower than 0"); 131: }
132:
133: Question question = null;
134:
135: StringBuilder queryQuizzBuilder = new StringBuilder();
136:
137: queryQuizzBuilder.Append(" <Where>"); 138: queryQuizzBuilder.Append(" <Eq>"); 139: queryQuizzBuilder.Append(" <FieldRef Name=\"ID\" />"); 140: queryQuizzBuilder.AppendFormat(" <Value Type=\"Counter\">{0}</Value>", index); 141: queryQuizzBuilder.Append(" </Eq>"); 142:
143: queryQuizzBuilder.Append(" </Where>"); 144:
145: string queryQuizzCaml = queryQuizzBuilder.ToString();
146:
147: SPQuery queryQuizz = new SPQuery();
148: queryQuizz.Query = queryQuizzCaml;
149: queryQuizz.RowLimit = 1;
150:
151: SPListItemCollection resultQuestion = this.quizzList.GetItems(queryQuizz);
152:
153: if (resultQuestion.Count > 0)
154: { 155: SPListItem itemQuizz = resultQuestion[0];
156:
157: question = new Question
158: { 159: Id = (int)itemQuizz[SPBuiltInFieldId.ID],
160: Content = itemQuizz[Fields.QuizzQuestion] as string,
161: Answers = this.GetAnswersForQuestion(itemQuizz),
162: Category = this.GetCategoryForQuestion(itemQuizz)
163: };
164: }
165:
166: return question;
167: }
168: #endregion
169: }
170: }
PS : l'objet Question est un objet métier crée par mes soins, composé d'un ID, d'un Contenu, de Réponses (définit par leur contenu et leur validité) et d'un Catégorie.
Bon alors tout ça, c'est très bien mais comment être réellement sur que ça fonctionne, que mes requêtes sont correctes, etc...
Et pourquoi on ne ferait pas des test unitaires qui nous permettraient de valider ces méthodes ?
Ok pourquoi pas, mais comment ? Et bien regardons déjà pour la première méthode : GetNextQuestionShouldReturnFirstQuestion.
1: using System.Collections.Generic;
2: using Microsoft.SharePoint;
3: using Microsoft.VisualStudio.TestTools.UnitTesting;
4: using Microsoft.VisualStudio.TestTools.UnitTesting.Web;
5: using TechDays.Demo.SharePoint.BusinessEntities;
6:
7: namespace TechDays.Demo.SharePoint.Model.Tests
8: { 9: /// <summary>
10: /// Test Class that will handle all the Tests for the Question Repository
11: /// </summary>
12: [TestClass]
13: public class QuestionRepositoryTests
14: { 15: /// <summary>
16: /// List of Questions
17: /// </summary>
18: private readonly List<Question> questions;
19:
20: /// <summary>
21: /// Initializes a new instance of the QuestionRepositoryTests class.
22: /// </summary>
23: public QuestionRepositoryTests()
24: { 25: this.questions = new List<Question>();
26:
27: #region Test Collection Initialization
28:
29: Question q1 = new Question()
30: { 31: Id = 1,
32: Content = "Dans quel pays, le chat domestique était vénéré à l'époque des pharaons ?",
33: Category = new Category() { Id = 1, Title = "Carnivore" }, 34: Answers = new List<Answer>()
35: { 36: new Answer() { Content = "Egypte", IsValid = true }, 37: new Answer() { Content = "Tunisie", IsValid = false }, 38: new Answer() { Content = "Espagne", IsValid = false } 39: }
40: };
41:
42: Question q2 = new Question()
43: { 44: Id = 2,
45: Content = "Quel est le reptile le plus grand ?",
46: Category = new Category() { Id = 3, Title = "Reptile" }, 47: Answers = new List<Answer>()
48: { 49: new Answer() { Content = "Cochon d'inde", IsValid = false }, 50: new Answer() { Content = "Autruche", IsValid = false }, 51: new Answer() { Content = "Crocodile", IsValid = true } 52: }
53: };
54: #endregion
55:
56: this.questions.Add(q1);
57: this.questions.Add(q2);
58: }
59:
60: /// <summary>
61: /// When GetNextQuestion(0) is called, it should return the first question.
62: /// </summary>
63: [TestMethod]
64: [HostType("ASP.Net")] 65: [UrlToTest("http://localhost/default.aspx")] 66: public void GetNextQuestionShouldReturnFirstQuestion()
67: { 68: Question expected = this.questions[0];
69: string listName = "Quizz Animaux";
70:
71: QuestionRepository repository = new QuestionRepository(SPContext.Current.Web, listName);
72:
73: Question result = repository.GetNextQuestion(0);
74:
75: Assert.AreEqual(expected.Content, result.Content);
76: Assert.AreEqual(expected.Category.Title, result.Category.Title);
77: Assert.AreEqual(expected.Answers.Count, result.Answers.Count);
78: Assert.AreEqual(expected.Answers[0].Content, result.Answers[0].Content);
79: Assert.AreEqual(expected.Answers[0].IsValid, result.Answers[0].IsValid);
80: Assert.AreEqual(expected.Answers[1].Content, result.Answers[1].Content);
81: Assert.AreEqual(expected.Answers[1].IsValid, result.Answers[1].IsValid);
82: Assert.AreEqual(expected.Answers[2].Content, result.Answers[2].Content);
83: Assert.AreEqual(expected.Answers[2].IsValid, result.Answers[2].IsValid);
84: }
85: }
86: }
Petit rappel important, pour que ce test fonctionne il faut savoir que j'ai actuellement un environnement de test déjà créé sur ma machine de développement (cf post précédent à ce sujet) avec toutes les données de tests dont j'ai besoin.(cf image ci-dessous)
Dans le constructeur QuestionRepositoryTests, j'initialise simplement une collection de réponses attendues afin que ce soit plus pratique à manipuler dans mes méthodes de tests.
Dans la méthode de test GetNextQuestionShouldReturnFirstQuestion, j'instancie mon repository "normalement", appelle la méthode GetNextQuestion "normalement" et vérifie simplement que la réponse récupérée est bien celle attendue.
La petite particularité se situe au niveau des attributs de la méthode de test GetNextQuestionShouldReturnFirstQuestion : [HostType("ASP.Net")] et [UrlToTest(http://localhost/default.aspx)]. Ces attributs me permettent de m'attacher à l'environnement SharePoint qui est sur ma machine de développement et faire les requêtes CAML comme si j'étais dans le contexte SharePoint.
Le seul inconvénient majeur étant, bien entendu, la dépendance sur l'environnement de test qui est toujours discutable pour les puristes.
Dans le prochain post, vous verrez qu'il est possible de faire des tests unitaires avec TypeMock qui nous permettront d'eviter cette dépendance.
<Philippe/>