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 3, The DomainServiceTestHost.
Introduction▲
Dans cette conclusion excitante de ma série sur les tests unitaires, je vais vous montrer comment utiliser la DomainServiceTestHost pour tester vos DomainService. Dans la première partie et la deuxième partie de cette série, je vous ai montré comment extraire les dépendances externes en utilisant l'IDomainServiceFactory et comment utiliser le pattern Repository. Maintenant que tout le travail de fond a été effectué, je vais vous montrer comment faire pour tester votre logique métier.
Le DomainService▲
Le DomainService que nous testons ressemble à ceci :
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.
public
class
BookClubDomainService :
RepositoryDomainService
{
public
BookClubDomainService
(
IUnitOfWork unitOfWork,
IBookRepository bookRepository,
ILibraryService libraryService,
IApprovalSystem 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
(
) {
...
}
// Test 1: Operation should return all books for category
// Test 2: Operation should return books ordered by BookID
public
IQueryable<
Book>
GetBooksForCategory
(
int
categoryId) {
...
}
// 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) {
...
}
// Test 1: Operation should update book
// Test 2: Operation should return validation errors
public
void
UpdateBook
(
Book book) {
...
}
// Test 1: Operation should update book
// Test 2: Operation should update the added date
[Update(UsingCustomMethod = true)]
public
void
AddNewEdition
(
Book book) {
...
}
// Test 1: Operation should delete book
// Test 2: Operation should require authentication
[RequiresAuthentication]
public
void
DeleteBook
(
Book book) {
...
}
// Test 1: Operation should return the most recent added date
public
DateTime GetLatestActivity
(
) {
...
}
}
Il possède les opérations Query, Insert, Update et Delete standard, en plus des opérations Query, Update, et Invoke (/ Service) personnalisées. Le constructeur accepte un certain nombre de paramètres, chacun représentant une dépendance externe dans le code. J'ai marqué chaque méthode avec les tests que nous allons écrire contre celle-ci afin de ne pas avoir à connaître les détails de l'implémentation (ils sont dans l'exemple, je fais juste l'impasse dessus dans cet article).
La DomainServiceTestHost▲
La DomainServiceTestHost est une nouvelle classe au sein de l'assembly Microsoft.ServiceModel.DomainServices.Server.UnitTesting (maintenant disponible sur NuGet et bientôt livrée avec le « Toolkit »). Elle est conçue pour vous aider à tester les opérations DomainService individuelles. L'API se rapproche particulièrement des conventions DomainService (et peut aider à les clarifier si vous êtes toujours un peu dans le flou). À titre d'exemple, l'hôte de test possède une méthode Query pour récupérer des données, et des méthodes Insert, Update et Delete pour les modifier.
Outre la prise en charge des opérations standard, l'hôte de test simplifie le test de la validation et de l'autorisation. Pour chaque signature standard dans la DomainServiceTestHost, il existe une variante TryXx qui facilite la capture des erreurs de validation. Par exemple, Query est associée à TryQuery et Insert à TryInsert. Aussi, à chaque fois que vous créez un hôte de test, vous pouvez passer un IPrincipal dans le constructeur avec lequel les opérations doivent être exécutées. Cela facilite le fait de parcourir un nombre d'utilisateurs de test pendant que vous validez vos métadonnées d'autorisation. Enfin, l'hôte de test peut être créé avec une méthode de fabrique qu'il utilise pour instancier un DomainService. Cela vous permet d'initialiser un DomainService avec des dépendances spécifiques aux tests.
Tester un DomainService▲
Enfin, nous arrivons à la partie intéressante. Dans cette section, je vais vous guider à travers les étapes nécessaires pour tester votre logique métier. D'abord, nous allons initialiser les variables locales que nous allons utiliser avec chaque test.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
[TestInitialize]
public
void
TestInitialize
(
)
{
this
.
_libraryService =
new
MockLibraryService
(
);
this
.
_approvalSystem =
new
FakeApprovalSystem
(
);
this
.
_unitOfWork =
new
FakeUnitOfWork
(
);
this
.
_bookRepository =
new
MockBookRepository
(
);
this
.
_domainServiceTestHost =
new
DomainServiceTestHost<
BookClubDomainService>(
this
.
CreateDomainService);
}
private
BookClubDomainService CreateDomainService
(
)
{
return
new
BookClubDomainService
(
this
.
_unitOfWork,
this
.
_bookRepository,
this
.
_libraryService,
this
.
_approvalSystem);
}
Comme vous pouvez le voir, j'ai écrit des types mock/objet factice/bouchon simples pour chaque dépendance. J'avais discuté un peu de MockBookRepository dans mon article précédent. Dans le contexte des tests suivants, il est important de souligner que j'ai initialisé le repository avec un jeu initial de données. Les trois autres sont de simples implémentations de tests. Aussi, j'ai fourni une méthode CreateDomainService que je peux passer à l'hôte de test qui initialise ma BookClubDomainService avec les dépendances de tests. Si vous n'êtes pas familier avec les tests de Visual Studio, la méthode [TestInitialize] sera appelée avant le début de chaque test.
En démarrant avec quelque chose de simple, nous allons jeter un œil au test de la requête par défaut.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
[TestMethod]
[Description(
"Tests that the GetBooks query returns all the books"
)]
public
void
GetBooks_ReturnsAllBooks
(
)
{
IEnumerable<
Book>
books =
this
.
_domainServiceTestHost.
Query
(
ds =>
ds.
GetBooks
(
));
Assert.
AreEqual
(
this
.
_bookRepository.
GetBooksWithCategories
(
).
Count
(
),
books.
Count
(
),
"Operation should return all books"
);
}
Dans cette méthode, nous demandons à l'hôte de test de renvoyer les résultats de la requête. Le paramètre « ds » dans l'expression lambda est l'instance du DomainService que nous testons. Comme l'IntelliSense pour l'opération de requête est un peu détaillé (comme c'est le cas dès que le type Expression se manifeste), je vais vous donner un autre exemple de sorte que vous vous y habituiez. Cette fois, nous testons la requête personnalisée.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
[TestMethod]
[Description(
"Tests that the GetBooksForCategory query orders books by BookID"
)]
public
void
GetBooksForCategory_OrderedByBookID
(
)
{
int
categoryId =
this
.
_bookRepository.
GetTable<
Category>(
).
First
(
).
CategoryID;
IEnumerable<
Book>
books =
this
.
_domainServiceTestHost.
Query
(
ds =>
ds.
GetBooksForCategory
(
categoryId));
Assert.
IsTrue
(
books.
OrderBy
(
b =>
b.
BookID).
SequenceEqual
(
books),
"Operation should return books ordered by BookID"
);
}
Dans cet extrait nous passons une variable locale à l'opération de requête, mais tout le reste est à peu près le même. Nous avons reçu la collection renvoyée et maintenant nous vérifions qu'elle est dans un ordre trié.
Les tests pour les opérations Insert, Update, Delete sont tout aussi faciles à écrire. Le simple fait d'appeler la méthode sur l'hôte redirigera vers l'opération correspondante dans votre 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.
26.
27.
28.
[TestMethod]
[Description(
"Tests that the InsertBook operation inserts a new book"
)]
public
void
InsertBook_InsertsNewBook
(
)
{
int
categoryId =
this
.
_bookRepository.
GetTable<
Category>(
).
First
(
).
CategoryID;
Book book =
new
Book
{
ASIN =
"1234567890"
,
Author =
"Author"
,
CategoryID =
categoryId,
Description =
"Description"
,
PublishDate =
DateTime.
UtcNow.
Subtract
(
TimeSpan.
FromDays
(
1
)),
Title =
"Title"
,
};
this
.
_domainServiceTestHost.
Insert
(
book);
Assert.
IsTrue
(
book.
BookID >
0
,
"New book should have a valid BookID"
);
Book addedBook =
this
.
_bookRepository.
GetEntities
(
).
Single
(
b =>
b.
BookID ==
book.
BookID);
Assert.
IsNotNull
(
addedBook,
"Operation should insert book"
);
}
Pour reformuler ce que j'ai dit ci-dessus, appeler Insert sur l'hôte de test avec un Book fait un appel à notre opération DomainService, InsertBook. Comme vous pouvez le voir, à la fin de ce test notre nouveau livre a été ajouté au repository.
En plus des méthodes Query, Insert, Update et Delete, des opérations Named Updates et Invoke sont également prises en charge. Dans les deux cas, la syntaxe est très similaire au test d'une Query.
2.
3.
4.
this
.
_domainServiceTestHost.
Update
(
ds =>
ds.
AddNewEdition
(
book),
original);
DateTime result =
this
.
_domainServiceTestHost.
Invoke
(
ds =>
ds.
GetLatestActivity
(
));
AddNewEdition met à jour le livre et exécute une logique métier personnalisée et GetLatestActivity renvoie une DateTime liée au tout dernier livre. Encore une fois, le paramètre « ds » dans l'expression lambda fait référence au DomainService en train d'être testé.
Tester la validation et l'autorisation▲
Tester les métadonnées de validation et d'autorisation pour une opération DomainService peut être tout aussi important que tester la logique métier. Une suite de tests unitaires vérifiant l'autorisation et la validation serait un excellent outil pour prévenir les régressions de sécurité du service.
La validation est assez simple à tester. Au lieu d'utiliser les méthodes de l'hôte de test que j'ai déjà décrites, vous devriez utiliser leurs variantes TryXx.
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.
[TestMethod]
[Description(
"Tests that the UpdateBook operation returns validation errors when passed an invalid book"
)]
public
void
UpdateBook_SetsValidationErrors
(
)
{
Book original =
this
.
_bookRepository.
GetEntities
(
).
First
(
);
Book book =
new
Book
{
AddedDate =
original.
AddedDate,
ASIN =
"Invalid!"
,
Author =
original.
Author,
BookID =
original.
BookID,
Category =
original.
Category,
CategoryID =
original.
CategoryID,
Description =
original.
Description,
PublishDate =
original.
PublishDate,
Title =
original.
Title,
};
IList<
ValidationResult>
validationErrors;
bool
success =
this
.
_domainServiceTestHost.
TryUpdate
(
book,
original,
out
validationErrors);
Assert.
IsFalse
(
success,
"Operation should have validation errors"
);
Assert.
AreEqual
(
1
,
validationErrors.
Count,
"Operation should return validation errors"
);
Assert.
IsTrue
(
validationErrors[
0
].
MemberNames.
Single
(
) ==
"ASIN"
,
"Operation should return a validation error for 'ASIN'"
);
}
Ce test fait appel à UpdateBook avec des données non valides, puis vérifie si les erreurs de validation qui en résultent sont celles que nous attendons.
Les tests d'autorisation suivent une approche différente. Au lieu d'utiliser une méthode d'hôte de test différente, ils définissent une entité personnalisée qui sera utilisée lors de l'appel de l'opération DomainService. Tandis que l'hôte de test prend par défaut un utilisateur anonyme, un constructeur alternatif vous permet de spécifier l'utilisateur, ce qui est intéressant pour votre cas de test.
2.
3.
4.
this
.
_domainServiceTestHost =
new
DomainServiceTestHost<
BookClubDomainService>(
this
.
CreateDomainService,
BookClubDomainServiceTest.
authenticatedUser);
Conclusion▲
Pour tout résumer, la DomainServiceTestHost en combinaison avec une IDomainServiceFactory et le pattern Repository simplifient le test unitaire de vos DomainService de façon isolée et fiable. De même, l'hôte de test n'étant qu'un type .NET, il devrait être compatible avec tous les outils de test et frameworks avec lesquels vous souhaitez l'utiliser. Espérons que cette série vous a donné un bon aperçu des tests unitaires d'un DomainService.
Une note sur la DomainServiceTestHost▲
Au moment de la version initiale de cet hôte de test, il peut toujours y avoir quelques cas limites pour des jeux de modification d'association et de composition qui ne sont pas pris en charge. Si vous en voyez, veuillez me faire savoir afin que je puisse mettre en place un scénario pour une version ultérieure.
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 _Max_ pour sa relecture orthographique et ses propositions.