SavePeople () doit-il être testé par unité
Oui, ça devrait. Mais essayez d’écrire vos conditions de test d’une manière indépendante de la mise en oeuvre. Par exemple, convertissez votre exemple d'utilisation en test unitaire:
function testSavePeople() {
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
assert(myDataStore.containsPerson('Joe'));
assert(myDataStore.containsPerson('Maggie'));
assert(myDataStore.containsPerson('John'));
}
Ce test fait plusieurs choses:
- il vérifie le contrat de la fonction
savePeople()
- il ne se soucie pas de la mise en œuvre de
savePeople()
- il documente l'exemple d'utilisation de
savePeople()
Prenez note que vous pouvez toujours simuler / tronquer / simuler le magasin de données. Dans ce cas, je ne vérifierais pas les appels de fonction explicites, mais le résultat de l'opération. De cette façon, mon test est préparé pour les modifications / refacteurs futurs.
Par exemple, votre implémentation de magasin de données pourrait fournir une saveBulkPerson()
méthode dans le futur. Désormais, une modification de l'implémentation savePeople()
à utiliser saveBulkPerson()
n'interromprait pas le test unitaire tant qu'elle saveBulkPerson()
fonctionnerait comme prévu. Et si d'une saveBulkPerson()
manière ou d'une autre ne fonctionne pas comme prévu, votre test unitaire le détectera.
ou de tels tests équivaudraient-ils à tester la construction de langage forEach intégrée?
Comme cela a été dit, essayez de tester les résultats attendus et l'interface de fonction, mais pas d'implémentation (à moins que vous ne fassiez des tests d'intégration - il pourrait alors être utile de capturer des appels de fonction spécifiques). S'il existe plusieurs façons d'implémenter une fonction, elles doivent toutes fonctionner avec votre test unitaire.
En ce qui concerne votre mise à jour de la question:
Testez les changements d'état! Par exemple, une partie de la pâte sera utilisée. Selon votre implémentation, affirmez que la quantité d’utilisé dough
convient pan
ou affirme que le dough
est épuisé. Vérifiez que le pan
contient des cookies après l'appel de la fonction. Assure que le oven
est vide / dans le même état que précédemment.
Pour des tests supplémentaires, vérifiez les cas de bord: Que se passe-t-il si le oven
n'est pas vide avant l'appel? Que se passe-t-il s'il n'y en a pas assez dough
? Si le pan
est déjà plein?
Vous devriez pouvoir déduire toutes les données requises pour ces tests à partir des objets de pâte, de casserole et de four eux-mêmes. Pas besoin de capturer les appels de fonction. Traitez la fonction comme si sa mise en œuvre ne serait pas disponible!
En fait, la plupart des utilisateurs de TDD écrivent leurs tests avant d’écrire la fonction, ils ne dépendent donc pas de la mise en oeuvre réelle.
Pour votre dernier ajout:
Lorsqu'un utilisateur crée un nouveau compte, un certain nombre de choses doivent se produire: 1) un nouvel enregistrement d'utilisateur doit être créé dans la base de données 2) un courrier électronique de bienvenue doit être envoyé 3) l'adresse IP de l'utilisateur doit être enregistrée pour fraude fins.
Nous voulons donc créer une méthode qui relie toutes les étapes du "nouvel utilisateur":
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
Pour une fonction comme celle-ci, je me moquerais de / stub / fake (tout ce qui semble plus général) des paramètres dataStore
et emailService
. Cette fonction ne fait aucune transition d'état sur aucun paramètre, elle les délègue aux méthodes de certains d'entre eux. Je voudrais essayer de vérifier que l'appel à la fonction a fait 4 choses:
- il a inséré un utilisateur dans le magasin de données
- il a envoyé (ou au moins appelé la méthode correspondante) un email de bienvenue
- il a enregistré l'adresse IP des utilisateurs dans le magasin de données
- il a délégué toute exception / erreur rencontrée (le cas échéant)
Les 3 premières vérifications peuvent être effectuées avec des faux, des moignons ou des faux ( dataStore
et emailService
vous ne voulez vraiment pas envoyer de courriels lors des tests). Depuis que j'ai dû chercher ceci pour certains commentaires, ce sont les différences:
- Un faux est un objet qui se comporte de la même manière que l'original et qui, dans une certaine mesure, est indiscernable. Son code peut normalement être réutilisé à travers les tests. Cela peut, par exemple, être une simple base de données en mémoire pour un wrapper de base de données.
- Un tronçon implémente simplement autant que nécessaire pour effectuer les opérations requises de ce test. Dans la plupart des cas, un talon est spécifique à un test ou à un groupe de tests ne nécessitant qu'un petit ensemble des méthodes de l'original. Dans cet exemple, il peut s'agir d'une application
dataStore
qui implémente simplement une version appropriée de insertUserRecord()
and recordIpAddress()
.
- Un mock est un objet qui vous permet de vérifier son utilisation (le plus souvent en vous permettant d'évaluer les appels à ses méthodes). J'essayerais de les utiliser avec parcimonie dans les tests unitaires car en les utilisant, vous essayez en fait de tester la mise en œuvre de la fonction et non l'adhérence de son interface, mais ils ont toujours leurs utilisations. Il existe de nombreux cadres de simulation pour vous aider à créer exactement le modèle dont vous avez besoin.
Notez que si l'une de ces méthodes génère une erreur, nous voulons que l'erreur apparaisse jusqu'au code appelant, de sorte qu'elle puisse la gérer comme elle l'entend. S'il est appelé par le code de l'API, cela peut traduire l'erreur en un code de réponse HTTP approprié. Si elle est appelée par une interface Web, l'erreur peut être traduite en un message approprié à afficher pour l'utilisateur, etc. Le fait est que cette fonction ne sait pas comment gérer les erreurs qui peuvent être générées.
Les exceptions / erreurs attendues sont des cas de test valides: vous confirmez que, si un tel événement se produit, la fonction se comporte comme prévu. Cela peut être réalisé en laissant l'objet simulé / faux / tronqué jeté lorsque vous le souhaitez.
L'essence de ma confusion est que, pour tester un peu une telle fonction, il semble nécessaire de répéter la mise en œuvre exacte dans le test lui-même (en précisant que les méthodes sont appelées dans un certain ordre), ce qui semble faux.
Parfois, cela doit être fait (même si cela vous tient particulièrement à cœur lors des tests d'intégration). Le plus souvent, il existe d'autres moyens de vérifier les effets secondaires / changements d'état attendus.
La vérification des appels de fonctions exactes donne lieu à des tests unitaires plutôt fragiles: seuls de petits changements apportés à la fonction d'origine entraînent leur échec. Cela peut être souhaité ou non, mais cela nécessite une modification du ou des tests unitaires correspondants chaque fois que vous modifiez une fonction (que ce soit le refactoring, l'optimisation, la correction de bugs, ...).
Malheureusement, dans ce cas, le test unitaire perd une partie de sa crédibilité: puisqu'il a été modifié, il ne confirme pas la fonction après le changement se comporte de la même manière qu'auparavant.
Par exemple, imaginons que quelqu'un ajoute un appel à oven.preheat()
(optimisation!) Dans votre exemple de préparation de biscuits:
- Si vous moquez l'objet du four, il n'attendra pas cet appel et échouera au test, bien que le comportement observable de la méthode n'ait pas changé (vous disposez toujours d'un pan de cookies, espérons-le).
- Un talon peut échouer ou non, selon que vous avez uniquement ajouté les méthodes à tester ou l'interface entière avec certaines méthodes factices.
- Un faux ne devrait pas échouer, car il devrait implémenter la méthode (en fonction de l'interface)
Dans mes tests unitaires, j'essaie d'être aussi général que possible: si la mise en œuvre change, mais que le comportement visible (du point de vue de l'appelant) est toujours le même, mes tests doivent réussir. Idéalement, le seul cas où je devrais modifier un test unitaire existant devrait être une correction de bogue (du test, pas de la fonction testée).