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é.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
public
class
BookClubDomainService :
LinqToEntitiesDomainService<
BookClubEntities>
{
private
readonly
LibraryService _libraryService =
new
LibraryService
(
);
private
readonly
ApprovalSystem _approvalSystem =
new
ApprovalSystem
(
);
// Test 1: Operation should return all books
// Test 2: Operation should return books with categories
// Test 3: Operation should return books ordered by BookID
public
IQueryable<
Book>
GetBooks
(
)
{
return
this
.
ObjectContext.
Books.
Include
(
"Category"
).
OrderBy
(
b =>
b.
BookID);
}
// Test 1: Operation should insert book
// Test 2: Operation should set the added date
// Test 3: Operation should request approval for book with invalid ASINs
// Test 4: Operation should request approval for book not yet published
public
void
InsertBook
(
Book book)
{
if
((
book.
EntityState !=
EntityState.
Detached))
{
this
.
ObjectContext.
ObjectStateManager.
ChangeObjectState
(
book,
EntityState.
Added);
}
else
{
this
.
ObjectContext.
Books.
AddObject
(
book);
}
book.
AddedDate =
DateTime.
UtcNow;
if
(!
this
.
_libraryService.
IsAsinValid
(
book.
ASIN))
{
this
.
_approvalSystem.
RequestApproval
(
book.
Author,
book.
Title,
book.
PublishDate);
}
else
if
(
book.
PublishDate >
book.
AddedDate)
{
this
.
_approvalSystem.
RequestApproval
(
book.
ASIN);
}
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
public
interface
ILibraryService
{
bool
IsAsinValid
(
string
asin);
}
public
interface
IApprovalSystem
{
void
RequestApproval
(
string
author,
string
title,
DateTime publishDate);
void
RequestApproval
(
string
asin);
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
public
class
BookClubDomainService :
LinqToEntitiesDomainService<
BookClubEntities>
{
private
readonly
ILibraryService _libraryService;
private
readonly
IApprovalSystem _approvalSystem;
public
BookClubDomainService
(
ILibraryService libraryService,
IApprovalSystem approvalSystem)
{
if
(
libraryService ==
null
)
{
throw
new
ArgumentNullException
(
"libraryService"
);
}
if
(
approvalSystem ==
null
)
{
throw
new
ArgumentNullException
(
"approvalSystem"
);
}
this
.
_libraryService =
libraryService;
this
.
_approvalSystem =
approvalSystem;
}
// Test 1: Operation should return all books
// Test 2: Operation should return books with categories
// Test 3: Operation should return books ordered by BookID
public
IQueryable<
Book>
GetBooks
(
)
{
return
this
.
ObjectContext.
Books.
Include
(
"Category"
).
OrderBy
(
b =>
b.
BookID);
}
// Test 1: Operation should insert book
// Test 2: Operation should set the added date
// Test 3: Operation should request approval for book with invalid ASINs
// Test 4: Operation should request approval for book not yet published
public
void
InsertBook
(
Book book)
{
if
((
book.
EntityState !=
EntityState.
Detached))
{
this
.
ObjectContext.
ObjectStateManager.
ChangeObjectState
(
book,
EntityState.
Added);
}
else
{
this
.
ObjectContext.
Books.
AddObject
(
book);
}
book.
AddedDate =
DateTime.
UtcNow;
if
(!
this
.
_libraryService.
IsAsinValid
(
book.
ASIN))
{
this
.
_approvalSystem.
RequestApproval
(
book.
Author,
book.
Title,
book.
PublishDate);
}
else
if
(
book.
PublishDate >
book.
AddedDate)
{
this
.
_approvalSystem.
RequestApproval
(
book.
ASIN);
}
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
public
class
BookClubDomainServiceFactory :
IDomainServiceFactory
{
public
DomainService CreateDomainService
(
Type domainServiceType,
DomainServiceContext context)
{
DomainService domainService;
if
(
typeof
(
BookClubDomainService) ==
domainServiceType)
{
domainService =
new
BookClubDomainService
(
new
LibraryService
(
),
new
ApprovalSystem
(
));
}
else
{
domainService =
(
DomainService)
Activator.
CreateInstance
(
domainServiceType);
}
domainService.
Initialize
(
context);
return
domainService;
}
public
void
ReleaseDomainService
(
DomainService domainService)
{
domainService.
Dispose
(
);
}
}
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.
2.
3.
4.
5.
6.
7.
public
class
Global :
System.
Web.
HttpApplication
{
protected
void
Application_Start
(
object
sender,
EventArgs e)
{
DomainService.
Factory =
new
BookClubDomainServiceFactory
(
);
}
}
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▲
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.