Tests unitaires d'un DomainService WCF RIA

Partie 2 - Le pattern Repository

Kyle McClellan

Traducteur

Deepin Prayag

Étant donné que WCF RIA Services emploie un « pipeline pattern » pour invoquer vos opérations DomainService, il n'est pas toujours évident de savoir comment les tester. Dans cette série d'articles nous allons voir un petit DomainService et comment le tester. Entre autres nous allons voir comment implémenter une IDomainServiceFactory personnalisée, comment implémenter le pattern Repository, et comment utiliser la DomainServiceTestHost.

Langage/Outil/Technologie : C#, WCF RIA Services

Commentez cet article : Commentez ♪

- Titre : Tests unitaires d'un DomainService WCF RIA

- Auteur : Kyle McClellan

- Traducteur : Deepin Prayag

- Parution : 30 novembre 2012

- Licence : Copyright ® 2012 Kyle McClellan. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Traduction

- Introduction

- Un exemple du Repository

- L'API Repository

- L'API UnitOfWork

- Un DomainService utilisant un Repository

- Tester avec un Repository

- Conclusion

- Liens

- Remerciements

Traduction

Cet article est la traduction la plus fidèle possible de l'article original de Kyle McClellan, Unit Testing a WCF RIA DomainService: Part 2, The Repository Pattern.

Introduction

Ceci est la deuxième partie d'une série que j'écris sur la façon de réaliser des tests unitaires sur vos DomainService. Dans la première partie, j'explique comment vous pourriez utiliser une IDomainServiceFactory pour factoriser des dépendances externes de votre code. Poursuivant dans le même sens, je vais utiliser cet article pour discuter de la factorisation de la dépendance à la base de données aussi. En général, refactoriser pour sortir les dépendances externes accentuera à la fois l'isolation et la cohérence de vos tests.

Le pattern que j'utilise pour factoriser les dépendances externes est appelé le « Repository Pattern ». Il est bien connu et se décline en de nombreuses variantes. La variante que j'ai choisie ici est celle qui cadre plutôt bien avec la façon dont RIA consomme une ObjectContext d'Entity Framework. Si vous utilisez une technologie d'accès aux données différente, n'hésitez pas à modifier ce pattern jusqu'à ce que vous obteniez quelque chose qui fonctionne pour votre scénario.

Un exemple du Repository

Avant que je ne rentre dans l'API Repository ou la configuration du DomainService, je voudrais montrer ce que l'introduction d'un repository pourrait faire aux opérations DomainService standard. Voici quelques opérations écrites directement contre une ObjectContext.

1. // Test 1: Operation should return all books

2. // Test 2: Operation should return books with categories

3. // Test 3: Operation should return books ordered by BookID

4. public IQueryable<Book> GetBooks()

5. {

6. return this.ObjectContext.Books.Include("Category").

7. OrderBy(b => b.BookID);

8. }

10. // Test 1: Operation should return all books for category

11. // Test 2: Operation should return books ordered by BookID

12. public IQueryable<Book> GetBooksForCategory(int categoryId)

13. {

14. return this.ObjectContext.Books.

15. Where(b => b.CategoryID == categoryId).OrderBy(b => b.BookID);

16. }

18. // Test 1: Operation should update book

19. // Test 2: Operation should return validation errors

20. public void UpdateBook(Book book)

21. {

22. this.ObjectContext.Books.AttachAsModified(

23. book, this.ChangeSet.GetOriginal(book));

24. }

En introduisant un repository dans ce code, les opérations vont être légèrement différentes.

1. // Test 1: Operation should return all books

2. // Test 2: Operation should return books with categories

3. // Test 3: Operation should return books ordered by BookID

4. public IQueryable<Book> GetBooks()

5. {

6. return this._bookRepository.GetBooksWithCategories().

7. OrderBy(b => b.BookID);

8. }

10. // Test 1: Operation should return all books for category

11. // Test 2: Operation should return books ordered by BookID

12. public IQueryable<Book> GetBooksForCategory(int categoryId)

13. {

14. return this._bookRepository.GetEntities().

15. Where(b => b.CategoryID == categoryId).OrderBy(b => b.BookID);

16. }

18. // Test 1: Operation should update book

19. // Test 2: Operation should return validation errors

20. public void UpdateBook(Book book)

21. {

22. this._bookRepository.Update(

23. book, this.ChangeSet.GetOriginal(book));

24. }

De manière générale, nous avons conservé l'implémentation existante. Cependant, vous pouvez maintenant constater que nous n'avons pas de dépendance directe sur ObjectContext. Au lieu de cela toute interaction est canalisée par le repository de livres. Faire cela nous permettra d'introduire une implémentation « mockée » du repository au moment des tests, ce qui va nous permettre de tester sans aucune dépendance sur une base de données.

L'API Repository

Je vais abréger l'API que je montre dans cet article. Il y a des types et des méthodes plus intéressants dans l'exemple, mais je veux maintenir cette conversation ciblée. Le premier type que nous devrions examiner est l'interface IRepository <T>.

1. public interface IRepository<T> : IRepository

2. {

3. IQueryable<T> GetEntities();

4. void Insert(T entity);

5. void Update(T entity, T original);

6. void Delete(T entity);

7. }

Un repository est un type simple qui remonte les méthodes pour les opérations Query, Insert, Update et Delete qui apparaissent habituellement dans les DomainService. De même, l'interface du repository présente une affinité par type. De manière spécifique, vous auriez une instance séparée pour chaque type d'entité racine (incidemment cela m'a semblé un peu détaillé, mais a également très bien fait l'affaire. Je vous laisse prendre la décision finale quant à savoir si ça vaut le supplément de frappe).

Voici une implémentation Repository partielle utilisant Entity Framework.

1. public class Repository<T> : IRepository<T> where T : EntityObject

2. {

3. public virtual IQueryable<T> GetEntities()

4. {

5. return this.ObjectSet;

6. }

8. public virtual void Insert(T entity)

9. {

10. if (entity.EntityState != EntityState.Detached)

11. {

12. this.ObjectContext.ObjectStateManager.

13. ChangeObjectState(entity, EntityState.Added);

14. }

15. else

16. {

17. this.ObjectSet.AddObject(entity);

18. }

19. }

20. }

Comme vous pouvez le voir, le repository contient le code familier nécessaire pour fonctionner avec l'ObjectContext et l'ObjectSet d'Entity Framework.

La prochaine classe à regarder est une interface de repository dérivée. Comme vous avez déjà pu le remarquer, notre repository de livres possède une méthode GetBooksWithCategories qui ne se trouve pas dans l'interface de base.

1. public interface IBookRepository : IRepository<Book>

2. {

3. IQueryable<Book> GetBooksWithCategories();

4. }

L'implémentation concrète de cette méthode va nous montrer pourquoi elle est intéressante.

1. public class BookRepository : Repository<Book>, IBookRepository

2. {

3. public BookRepository(BookClubEntities objectContext)

4. : base(objectContext)

5. {

6. }

8. public IQueryable<Book> GetBooksWithCategories()

9. {

10. return this.ObjectSet.Include("Category");

11. }

12. }

De manière spécifique, nous avons utilisé la méthode Include sur ObjectSet. Étant donné que la méthode Include est spécifique à Entity Framework, je l'ai placée derrière l'interface du repository.

L'API UnitOfWork

Les DomainService et les Repository fonctionnent sur le principe que vous pouvez y apporter plusieurs changements, puis tous les soumettre en tant qu'une seule unité de travail. Afin de faciliter cela, notre pattern Repository fournit également une interface IUnitOfWork.

1. public interface IUnitOfWork

2. {

3. void Save();

4. }

Simple. Il nous suffit maintenant de s'assurer qu'elle est appelée quand notre DomainService est prêt à faire persister les modifications apportées. Heureusement que la classe de base DomainService fournit une méthode virtuelle protégée qui nous permet justement de le faire.

1. // Test 1: Unit of work should be saved

2. protected override bool PersistChangeSet()

3. {

4. this.UnitOfWork.Save();

5. return true;

6. }

Enfin, nous devons nous assurer que l'unité de travail peut être utilisée pour pérenniser les modifications apportées à notre modèle Entity Framework. Pour ce faire, nous pouvons utiliser une classe partielle pour implémenter l'interface sur BookClubEntities.

1. public partial class BookClubEntities : IUnitOfWork

2. {

3. public void Save()

4. {

5. this.SaveChanges();

6. }

7. }

Maintenant nous devrons jeter un œil à la façon dont tous ces éléments sont connectés.

Un DomainService utilisant un Repository

Comme je l'ai montré dans mon article précédent, nous allons devoir passer le repository au constructeur du DomainService.

1. public BookClubDomainService(

2. IUnitOfWork unitOfWork,

3. IBookRepository bookRepository,

4. ILibraryService libraryService,

5. IApprovalSystem approvalSystem)

6. : base(unitOfWork, bookRepository)

7. {

8. if (bookRepository == null)

9. {

10. throw new ArgumentNullException("bookRepository");

11. }

12. if (libraryService == null)

13. {

14. throw new ArgumentNullException("libraryService");

15. }

16. if (approvalSystem == null)

17. {

18. throw new ArgumentNullException("approvalSystem");

19. }

21. this._bookRepository = bookRepository;

22. this._libraryService = libraryService;

23. this._approvalSystem = approvalSystem;

24. }

Vous pouvez vous reportez à mon article précédent dans la série sur comment créer et « bootstrapper » une IDomainServiceFactory. Je ne vais pas reproduire l'information ici, mais je vais vous donner un rapide aperçu du code de fabrique qui crée le DomainService.

1. public class BookClubDomainServiceFactory : IDomainServiceFactory

2. {

3. public DomainService CreateDomainService(

4. Type domainServiceType, DomainServiceContext context)

5. {

6. DomainService domainService;

7. if (typeof(BookClubDomainService) == domainServiceType)

8. {

9. BookClubEntities bookClubEntities = new BookClubEntities();

10. domainService = new BookClubDomainService(

11. bookClubEntities,

12. new BookRepository(bookClubEntities),

13. new LibraryService(),

14. new ApprovalSystem());

15. }

16. else

17. {

18. domainService = (DomainService)

19. Activator.CreateInstance(domainServiceType);

20. }

22. domainService.Initialize(context);

23. return domainService;

24. }

25. }

Enfin, nous devons définir la BookClubDomainService afin de fournir les métadonnées correctes sur les types qu'elle prend en charge. Puisque nous renvoyons des types d'Entity Framework, nous devrons utiliser l'attribut LinqToEntitiesDomainServiceDescriptionProvider.

1. [LinqToEntitiesDomainServiceDescriptionProvider(

2. typeof(BookClubEntities))]

3. public class BookClubDomainService : RepositoryDomainService { ... }

De même, vous remarquerez que j'ai créé une classe de base RepositoryDomainService pour encapsuler certaines des préoccupations communes au Repository. Avec toutes ces pièces ensemble, cela devient une simple affaire pour écrire les opérations DomainService.

1. // Test 1: Operation should return all books

2. // Test 2: Operation should return books with categories

3. // Test 3: Operation should return books ordered by BookID

4. public IQueryable<Book> GetBooks()

5. {

6. return this._bookRepository.GetBooksWithCategories().

7. OrderBy(b => b.BookID);

8. }

Tester avec un Repository

Bien que j'aie l'intention de garder la plupart de ceci pour le prochain article, il est intéressant de jeter un œil au MockRepository que nous allons utiliser pour les tests.

1. public class MockRepository<T> : IRepository<T>

2. {

3. private readonly Dictionary<Type, IList> _entities =

4. new Dictionary<Type, IList>();

6. public List<TEntity> GetTable<TEntity>() { ... }

8. public virtual IQueryable<T> GetEntities()

9. {

10. return this.GetTable<T>().AsQueryable();

11. }

13. public virtual void Insert(T entity)

14. {

15. this.GetTable<T>().Add(entity);

16. }

17. }

Le « mock repository » nous permet de remplir notre table avec des données par défaut avant un test et de vérifier le contenu mis à jour de la table après le test.

1. private class MockBookRepository : MockRepository<Book>, IBookRepository

2. {

3. public MockBookRepository()

4. {

5. this.GetTable<Book>().AddRange(new []

6. {

7. new Book

8. {

9. AddedDate = DateTime.UtcNow,

10. ASIN = "1234567890",

11. Author = "Author",

12. BookID = 1,

13. ...

14. },

15. ...

16. });

17. }

18. }

Maintenant, lorsque nous créons notre DomainService lors de nos tests, nous pouvons passer une instance de MockBookRepository au lieu de notre implémentation réelle. L'utilisation d'un repository de ce genre dans nos tests améliore l'isolation et la cohérence. Dans le dernier article de cette série sur les tests, je vais enfin vous montrer comment écrire des tests unitaires pour vos DomainService.

Conclusion

Ceci conclut donc la deuxième partie de cette série qui nous a permis de voir comment implémenter le pattern Repository.

Liens

- Les sources de l'exemple sur les tests unitaires

- NuGet: RIAServices.UnitTesting

Remerciements

Je tiens ici à remercier Kyle McClellan de m'avoir autorisé à traduire son article.
Je remercie tomlev pour sa relecture technique et ses propositions.
Je remercie également ClaudeLELOUP pour sa relecture orthographique et ses propositions.