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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
// 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 return all books for category
// Test 2: Operation should return books ordered by BookID
public
IQueryable<
Book>
GetBooksForCategory
(
int
categoryId)
{
return
this
.
ObjectContext.
Books.
Where
(
b =>
b.
CategoryID ==
categoryId).
OrderBy
(
b =>
b.
BookID);
}
// Test 1: Operation should update book
// Test 2: Operation should return validation errors
public
void
UpdateBook
(
Book book)
{
this
.
ObjectContext.
Books.
AttachAsModified
(
book,
this
.
ChangeSet.
GetOriginal
(
book));
}
En introduisant un repository dans ce code, les opérations vont être légèrement différentes.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
// 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
.
_bookRepository.
GetBooksWithCategories
(
).
OrderBy
(
b =>
b.
BookID);
}
// Test 1: Operation should return all books for category
// Test 2: Operation should return books ordered by BookID
public
IQueryable<
Book>
GetBooksForCategory
(
int
categoryId)
{
return
this
.
_bookRepository.
GetEntities
(
).
Where
(
b =>
b.
CategoryID ==
categoryId).
OrderBy
(
b =>
b.
BookID);
}
// Test 1: Operation should update book
// Test 2: Operation should return validation errors
public
void
UpdateBook
(
Book book)
{
this
.
_bookRepository.
Update
(
book,
this
.
ChangeSet.
GetOriginal
(
book));
}
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>.
2.
3.
4.
5.
6.
7.
public
interface
IRepository<
T>
:
IRepository
{
IQueryable<
T>
GetEntities
(
);
void
Insert
(
T entity);
void
Update
(
T entity,
T original);
void
Delete
(
T entity);
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
public
class
Repository<
T>
:
IRepository<
T>
where
T :
EntityObject
{
public
virtual
IQueryable<
T>
GetEntities
(
)
{
return
this
.
ObjectSet;
}
public
virtual
void
Insert
(
T entity)
{
if
(
entity.
EntityState !=
EntityState.
Detached)
{
this
.
ObjectContext.
ObjectStateManager.
ChangeObjectState
(
entity,
EntityState.
Added);
}
else
{
this
.
ObjectSet.
AddObject
(
entity);
}
}
}
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.
2.
3.
4.
public
interface
IBookRepository :
IRepository<
Book>
{
IQueryable<
Book>
GetBooksWithCategories
(
);
}
L'implémentation concrète de cette méthode va nous montrer pourquoi elle est intéressante.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
public
class
BookRepository :
Repository<
Book>,
IBookRepository
{
public
BookRepository
(
BookClubEntities objectContext)
:
base
(
objectContext)
{
}
public
IQueryable<
Book>
GetBooksWithCategories
(
)
{
return
this
.
ObjectSet.
Include
(
"Category"
);
}
}
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.
2.
3.
4.
public
interface
IUnitOfWork
{
void
Save
(
);
}
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.
2.
3.
4.
5.
6.
// Test 1: Unit of work should be saved
protected
override
bool
PersistChangeSet
(
)
{
this
.
UnitOfWork.
Save
(
);
return
true
;
}
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.
2.
3.
4.
5.
6.
7.
public
partial
class
BookClubEntities :
IUnitOfWork
{
public
void
Save
(
)
{
this
.
SaveChanges
(
);
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
public
BookClubDomainService
(
IUnitOfWork unitOfWork,
IBookRepository bookRepository,
ILibraryService libraryService,
IApprovalSystem approvalSystem)
:
base
(
unitOfWork,
bookRepository)
{
if
(
bookRepository ==
null
)
{
throw
new
ArgumentNullException
(
"bookRepository"
);
}
if
(
libraryService ==
null
)
{
throw
new
ArgumentNullException
(
"libraryService"
);
}
if
(
approvalSystem ==
null
)
{
throw
new
ArgumentNullException
(
"approvalSystem"
);
}
this
.
_bookRepository =
bookRepository;
this
.
_libraryService =
libraryService;
this
.
_approvalSystem =
approvalSystem;
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
public
class
BookClubDomainServiceFactory :
IDomainServiceFactory
{
public
DomainService CreateDomainService
(
Type domainServiceType,
DomainServiceContext context)
{
DomainService domainService;
if
(
typeof
(
BookClubDomainService) ==
domainServiceType)
{
BookClubEntities bookClubEntities =
new
BookClubEntities
(
);
domainService =
new
BookClubDomainService
(
bookClubEntities,
new
BookRepository
(
bookClubEntities),
new
LibraryService
(
),
new
ApprovalSystem
(
));
}
else
{
domainService =
(
DomainService)
Activator.
CreateInstance
(
domainServiceType);
}
domainService.
Initialize
(
context);
return
domainService;
}
}
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.
2.
3.
[LinqToEntitiesDomainServiceDescriptionProvider(
typeof
(
BookClubEntities))]
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.
2.
3.
4.
5.
6.
7.
8.
// 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
.
_bookRepository.
GetBooksWithCategories
(
).
OrderBy
(
b =>
b.
BookID);
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public
class
MockRepository<
T>
:
IRepository<
T>
{
private
readonly
Dictionary<
Type,
IList>
_entities =
new
Dictionary<
Type,
IList>(
);
public
List<
TEntity>
GetTable<
TEntity>(
) {
...
}
public
virtual
IQueryable<
T>
GetEntities
(
)
{
return
this
.
GetTable<
T>(
).
AsQueryable
(
);
}
public
virtual
void
Insert
(
T entity)
{
this
.
GetTable<
T>(
).
Add
(
entity);
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
private
class
MockBookRepository :
MockRepository<
Book>,
IBookRepository
{
public
MockBookRepository
(
)
{
this
.
GetTable<
Book>(
).
AddRange
(
new
[]
{
new
Book
{
AddedDate =
DateTime.
UtcNow,
ASIN =
"1234567890"
,
Author =
"Author"
,
BookID =
1
,
...
},
...
}
);
}
}
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▲
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.