Tests unitaires d'un DomainService WCF RIA

Partie 1 - L'IDomainServiceFactory

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 : 29 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 DomainService avec des dépendances

- Factoriser les dépendances externes

- IDomainServiceFactory

- Une note sur l'injection de dépendances

- Une conception alternative

- 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 1, The IDomainServiceFactory.

Introduction

J'ai toujours été un partisan de piloter la qualité des produits par le biais des tests unitaires. Indépendamment d'une méthodologie spécifique de test, j'apprécie la confiance qu'un riche ensemble de tests peut apporter au développement du produit et des applications. À l'intention des développeurs WCF RIA, j'ai voulu démarrer une série d'articles en trois parties sur la façon de tester la logique métier qui réside au cœur de votre application ; vos opérations DomainService. Au cours de cette série, je vais identifier des pattern, des pratiques et des outils qui rendent simples les tests de vos DomainService (voire amusants ?).

La première étape afin de rendre votre DomainService testable est d'identifier les dépendances externes.

Un DomainService avec des dépendances

Nous allons commencer par examiner un service qui a une logique métier et des dépendances. Je l'ai rédigé d'une manière assez simple afin de pouvoir mettre en évidence des modifications spécifiques qui permettront d'améliorer la testabilité.

1. public class BookClubDomainService :

2. LinqToEntitiesDomainService<BookClubEntities>

3. {

4. private readonly LibraryService _libraryService = new LibraryService();

5. private readonly ApprovalSystem _approvalSystem = new ApprovalSystem();

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

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

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

10. public IQueryable<Book> GetBooks()

11. {

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

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

14. }

16. // Test 1: Operation should insert book

17. // Test 2: Operation should set the added date

18. // Test 3: Operation should request approval for book with invalid ASINs

19. // Test 4: Operation should request approval for book not yet published

20. public void InsertBook(Book book)

21. {

22. if ((book.EntityState != EntityState.Detached))

23. {

24. this.ObjectContext.ObjectStateManager.

25. ChangeObjectState(book, EntityState.Added);

26. }

27. else

28. {

29. this.ObjectContext.Books.AddObject(book);

30. }

32. book.AddedDate = DateTime.UtcNow;

34. if (!this._libraryService.IsAsinValid(book.ASIN))

35. {

36. this._approvalSystem.

37. RequestApproval(book.Author, book.Title, book.PublishDate);

38. }

39. else if (book.PublishDate > book.AddedDate)

40. {

41. this._approvalSystem.RequestApproval(book.ASIN);

42. }

43. }

44. }

Un coup d'œil sur ce service devrait révéler trois dépendances concrètes. D'abord, nous utilisons une ObjectContext d'Entity Framework pour communiquer avec une base de données. Nous avons aussi une dépendance sur un service externe, LibraryService, et un sous-système interne, ApprovalSystem. ObjectContext est un cas unique que j'aborderai dans la deuxième partie de cette série, donc pour l'instant, voyons ce que nous pouvons faire avec LibraryService et ApprovalSystem.

Plusieurs options sont disponibles pour tester du code ayant des dépendances externes. La première est de tester contre des composants réels ; peut-être avec des chaînes de connexion modifiées pour tester les instances. Vous pourriez, par exemple, exécuter vos tests contre une instance de test de la base de données. La seconde est d'utiliser un framework de « mocking » pour fournir des implémentations « mockées » pour les méthodes que vous utilisez. Vous pourriez, par exemple, écrire une implémentation rapide de LibraryService.IsAsinValid à utiliser uniquement avec vos tests. J'utilise Moles depuis tout récemment, mais il y a beaucoup d'autres bons framework qui sont disponibles. Enfin, vous pouvez utiliser un pattern nommé injection de dépendances (« Dependency Injection » en anglais) pour permettre à votre code de service de recevoir des références à des dépendances externes.

Le reste de cet article portera sur la façon d'utiliser l'interface IDomainServiceFactory afin de permettre l'injection de dépendances. Il y a plein de choses à dire sur les autres options de tests que j'ai énumérées ci-dessus, mais je ne le ferai pas ici. Un peu de recherche devrait être en mesure de vous aider à déterminer si et quand l'une des autres options vous convient.

Factoriser les dépendances externes

Maintenant que nous avons identifié nos dépendances externes, nous pouvons légèrement modifier la conception afin de leur permettre d'être passées au DomainService. Au lieu d'utiliser les types concrets, LibraryService et ApprovalSystem, nous allons les remplacer par des interfaces.

1. public interface ILibraryService

2. {

3. bool IsAsinValid(string asin);

4. }

5. public interface IApprovalSystem

6. {

7. void RequestApproval(string author, string title, DateTime publishDate);

8. void RequestApproval(string asin);

9. }

Ensuite, nous allons mettre à jour le DomainService pour utiliser ces interfaces. Plus important encore, nous allons mettre à jour le service pour imposer que ces dépendances soient fournies au constructeur.

1. public class BookClubDomainService :

2. LinqToEntitiesDomainService<BookClubEntities>

3. {

4. private readonly ILibraryService _libraryService;

5. private readonly IApprovalSystem _approvalSystem;

7. public BookClubDomainService(

8. ILibraryService libraryService, IApprovalSystem approvalSystem)

9. {

10. if (libraryService == null)

11. {

12. throw new ArgumentNullException("libraryService");

13. }

14. if (approvalSystem == null)

15. {

16. throw new ArgumentNullException("approvalSystem");

17. }

19. this._libraryService = libraryService;

20. this._approvalSystem = approvalSystem;

21. }

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

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

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

26. public IQueryable<Book> GetBooks()

27. {

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

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

30. }

32. // Test 1: Operation should insert book

33. // Test 2: Operation should set the added date

34. // Test 3: Operation should request approval for book with invalid ASINs

35. // Test 4: Operation should request approval for book not yet published

36. public void InsertBook(Book book)

37. {

38. if ((book.EntityState != EntityState.Detached))

39. {

40. this.ObjectContext.ObjectStateManager.

41. ChangeObjectState(book, EntityState.Added);

42. }

43. else

44. {

45. this.ObjectContext.Books.AddObject(book);

46. }

48. book.AddedDate = DateTime.UtcNow;

50. if (!this._libraryService.IsAsinValid(book.ASIN))

51. {

52. this._approvalSystem.

53. RequestApproval(book.Author, book.Title, book.PublishDate);

54. }

55. else if (book.PublishDate > book.AddedDate)

56. {

57. this._approvalSystem.RequestApproval(book.ASIN);

58. }

59. }

60. }

Si vous essayiez de lancer ce service maintenant, vous verriez qu'il échoue avec un message d'erreur du type « Aucun constructeur par défaut n'existe pour le type BookClubDomainService ». Pour corriger cette erreur, nous devrons comprendre un peu plus la façon dont les DomainService sont instanciés.

IDomainServiceFactory

Chaque fois qu'un client fait un appel dans le point de terminaison du DomainService, la couche d'hébergement RIA utilise le singleton DomainService.Factory pour créer une nouvelle instance du type DomainService demandé. L'implémentation IDomainServiceFactory par défaut attend d'un type DomainService qu'il fournisse un constructeur sans paramètre. Puisque nous avons changé le constructeur ci-dessus pour exiger deux paramètres, nous devons maintenant créer un type de fabrique qui peut gérer notre DomainService.

Les fabriques sont relativement simples à écrire et exposent uniquement une paire de méthodes Create/Release.

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. domainService = new BookClubDomainService(

10. new LibraryService(), new ApprovalSystem());

11. }

12. else

13. {

14. domainService = (DomainService)

15. Activator.CreateInstance(domainServiceType);

16. }

18. domainService.Initialize(context);

19. return domainService;

20. }

22. public void ReleaseDomainService(DomainService domainService)

23. {

24. domainService.Dispose();

25. }

26. }

Maintenant que nous avons une fabrique personnalisée, nous allons devoir nous assurer qu'elle est utilisée pour instancier notre DomainService. La meilleure façon de réaliser cela est d'ajouter un fichier Global.asax à notre site Web et d'y « bootstrapper » la fabrique.

1. public class Global : System.Web.HttpApplication

2. {

3. protected void Application_Start(object sender, EventArgs e)

4. {

5. DomainService.Factory = new BookClubDomainServiceFactory();

6. }

7. }

Maintenant, nous pouvons de nouveau exécuter notre service avec succès. La mise à jour que nous avons apportée au DomainService va nous permettre de lui passer des implémentations des dépendances externes spécifiques aux tests. Ceci à son tour améliore l'isolation et la cohérence de nos tests. Dans les deuxième et troisième parties de cette série, je montrerai comment traiter votre couche d'accès aux données en tant que dépendance externe et la façon réelle d'écrire les tests.

Une note sur l'injection de dépendances

Souvent, l'injection de dépendances se fait de façon plus générique en utilisant des framework d'injection. Si vous vous retrouvez à créer un gros changement dans le IDomainServiceFactory.CreateDomainService pour chaque type DomainService dans votre fabrique personnalisée, vous trouverez cela peut-être plus facile à maintenir en utilisant un framework. Il en existe plein, donc il ne devrait pas être trop difficile de trouver celui qui fonctionne pour vous.

Une conception alternative

Maintenant que vous avez patiemment parcouru cet article en entier, il est intéressant de noter qu'une IDomainServiceFactory personnalisée n'est pas strictement nécessaire afin de rendre votre DomainService testable. Par exemple, vous pourriez fournir deux constructeurs pour chaque DomainService ; un constructeur paramétré pour les tests et un constructeur par défaut qui choisit une valeur par défaut pour chaque dépendance. Il y a des avantages à l'approche de fabriques (telle que l'injection de dépendances), mais je vais vous laisser choisir celle qui convient le mieux.

Conclusion

Ceci conclut donc cette première partie de cette série qui nous a permis de voir comment implémenter une IDomainServiceFactory personnalisée.

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.