Pourquoi ai-je besoin de tests unitaires pour tester les méthodes du référentiel?

19

J'ai besoin de jouer un peu l'avocat des démons sur cette question car je ne peux pas bien la défendre par manque d'expérience. Voici l'affaire, j'obtiens conceptuellement les différences entre les tests unitaires et les tests d'intégration. En se concentrant spécifiquement sur les méthodes de persistance et le référentiel, un test unitaire utiliserait une maquette éventuellement via un framework comme Moq pour affirmer qu'un ordre recherché a été renvoyé comme prévu.

Disons que j'ai construit le test unitaire suivant:

[TestMethod]
public void GetOrderByIDTest()
{
   //Uses Moq for dependency for getting order to make sure 
   //ID I set up in 'Arrange' is same one returned to test in 'Assertion'
}

Donc, si je mets en place OrderIdExpected = 5et mon objet maquette revient 5comme l'ID mon test passera. J'ai compris. J'ai testé le code à l' unité pour m'assurer que ce que mon code préforme renvoie l'objet et l'ID attendus et pas autre chose.

L'argument que j'obtiendrai est le suivant:

"Pourquoi ne pas simplement sauter les tests unitaires et faire des tests d'intégration? C'est tester la procédure stockée de la base de données et votre code ensemble qui est important. Cela semble trop de travail supplémentaire d'avoir des tests unitaires et des tests d'intégration quand finalement je veux savoir si la base de données appelle et le code fonctionne. Je sais que les tests prennent plus de temps, mais ils doivent être exécutés et testés malgré tout, il me semble donc inutile d'avoir les deux. Il suffit de tester ce qui compte. "

Je pourrais le défendre avec une définition de manuel comme: "Eh bien, c'est un test d'intégration et nous devons tester le code séparément comme un test unitaire et, yada, yada, yada ..." C'est un cas où une explication puriste des pratiques contre la réalité se perd. Je rencontre parfois cela et si je ne peux pas défendre le raisonnement derrière le code de test unitaire qui repose finalement sur des dépendances externes, alors je ne peux pas le justifier.

Toute aide sur cette question est grandement appréciée, merci!

atconway
la source
2
je dis l'envoyer directement aux tests des utilisateurs ... ils vont de toute façon modifier les exigences ...
nathan hayfield
+1 pour le sarcasme pour garder la lumière de temps en temps
atconway
2
La réponse simple est que chaque méthode peut avoir x nombre de cas marginaux (disons que vous voulez tester des ID positifs, un ID de 0 et des ID négatifs). Si vous vouliez tester une fonctionnalité qui utilise cette méthode, qui a elle-même 3 cas de bord, vous devrez écrire 9 cas de test pour tester chaque combinaison de cas de bord. En les isolant, vous n'avez qu'à écrire 6. De plus, les tests vous donnent une idée plus précise de la raison pour laquelle quelque chose s'est cassé. Peut-être que votre référentiel renvoie null en cas d'échec, et l'exception null est levée plusieurs centaines de lignes dans le code.
Rob
Je pense que vous êtes trop strict sur votre définition d'un test "unitaire". Qu'est-ce qu'une "unité d'oeuvre" pour une classe de référentiel?
Caleb
Et assurez-vous d'en tenir compte: si votre test unitaire simule tout ce que vous essayez de tester, que testez-vous vraiment?
Caleb

Réponses:

20

Les tests unitaires et les tests d'intégration ont des objectifs différents.

Les tests unitaires vérifient la fonctionnalité de votre code ... que vous obtenez ce que vous attendez de la méthode lorsque vous l'appelez. Les tests d'intégration testent le comportement du code lorsqu'il est combiné en tant que système. Vous ne vous attendriez pas à ce que des tests unitaires évaluent le comportement du système, ni à des tests d'intégration pour vérifier des sorties spécifiques d'une méthode particulière.

Les tests unitaires, lorsqu'ils sont effectués correctement, sont plus faciles à configurer que les tests d'intégration. Si vous vous fiez uniquement aux tests d'intégration, vos tests vont:

  1. Soyez plus difficile à écrire, dans l'ensemble,
  2. Soyez plus fragile, en raison de toutes les dépendances requises, et
  3. Offrez moins de couverture de code.

Bottom line: Utiliser les tests d'intégration pour vérifier que les connexions entre les objets fonctionnent correctement, mais se pencher sur les tests unitaires d' abord pour vérifier les exigences fonctionnelles.


Cela dit, vous n'aurez peut-être pas besoin de tests unitaires pour certaines méthodes de référentiel. Les tests unitaires ne doivent pas être effectués sur des méthodes triviales ; si tout ce que vous faites est de passer par une demande d'objet au code généré par ORM et de retourner un résultat, vous n'avez pas besoin de le tester unitaire, dans la plupart des cas; un test d'intégration est suffisant.

Robert Harvey
la source
Oui merci pour la réponse rapide je l'apprécie. Ma seule critique de la réponse est qu'elle relève de la préoccupation de mon dernier paragraphe. Mon opposition n'entendra que des explications et des définitions abstraites et non un raisonnement plus approfondi. Par exemple, pourriez-vous fournir un raisonnement appliqué à mon cas d'utilisation de test de la procédure stockée / DB par rapport à mon code et pourquoi les tests unitaires sont toujours utiles en référence à ce cas d'utilisation particulier ?
atconway
1
Si le SP renvoie simplement un résultat de la base de données, un test d'intégration peut être adéquat. S'il contient une logique non triviale, des tests unitaires sont indiqués. Voir également stackoverflow.com/questions/1126614/… et msdn.microsoft.com/en-us/library/aa833169(v=vs.100).aspx (spécifique à SQL Server).
Robert Harvey
12

Je suis du côté des pragmatiques. Ne testez pas votre code deux fois.

Nous écrivons uniquement des tests d'intégration pour nos référentiels. Ces tests ne dépendent que d'une configuration de test simple qui s'exécute sur une base de données en mémoire. Je pense qu'ils fournissent tout ce qu'un test unitaire fait, et plus encore.

  1. Ils peuvent remplacer les tests unitaires lors du TDD. Bien qu'il y ait un peu plus de code de test à écrire avant de pouvoir commencer avec le vrai code, cela fonctionne très bien avec l'approche rouge / vert / refactor une fois que tout est en place.
  2. Ils testent le vrai code du référentiel - le code contenu dans les chaînes SQL ou les commandes ORM. Il est plus important de vérifier que la requête est correcte que de vérifier que vous avez réellement envoyé une chaîne à un StatementExecutor.
  3. Ils sont excellents comme tests de régression. S'ils échouent, cela est toujours dû à un problème réel, comme une modification du schéma qui n'a pas été prise en compte.
  4. Ils sont indispensables lors de la modification du schéma de base de données. Vous pouvez être sûr que vous n'avez pas modifié le schéma d'une manière qui rompt votre application tant que le test réussit. (Le test unitaire est inutile dans ce cas, car lorsque la procédure stockée n'existe plus, le test unitaire passe toujours.)
jaseur
la source
Très pragmatique en effet. J'ai constaté que la base de données en mémoire se comportait en réalité différemment (pas en termes de performances) de ma base de données réelle, en raison de l'ORM légèrement différent. Prenez-en soin dans vos tests d'intégration.
Marcel
1
@Marcel, j'ai rencontré ça. Je l'ai résolu en exécutant parfois tous mes tests sur une vraie base de données.
Winston Ewert
4

Les tests unitaires offrent un niveau de modularité que les tests d'intégration (par conception) ne peuvent pas. Lorsqu'un système est refactorisé ou recomposé (et cela se produira), les tests unitaires peuvent souvent être réutilisés, tandis que les tests d'intégration doivent souvent être réécrits. Les tests d'intégration qui essaient de faire le travail de quelque chose qui devrait être dans un test unitaire en font souvent trop, ce qui les rend difficiles à maintenir.

De plus, l'inclusion de tests unitaires présente les avantages suivants:

  1. Les tests unitaires vous permettent de décomposer rapidement un test d'intégration ayant échoué (peut-être en raison d'un bogue de régression) et d'identifier la cause. De plus, cela communiquera le problème à toute l'équipe plus rapidement que les diagrammes ou autres documents.
  2. Les tests unitaires peuvent servir d'exemples et de documentation (un type de documentation qui se compile réellement ) avec votre code. Contrairement à d'autres documents, vous saurez instantanément quand elle est obsolète.
  3. Les tests unitaires peuvent servir d'indicateurs de performance de base lorsque vous essayez de décomposer des problèmes de performance plus importants, tandis que les tests d'intégration nécessitent généralement beaucoup d'instruments pour trouver le problème.

Il est possible de diviser votre travail (et d'appliquer une approche DRY) tout en utilisant les tests unitaires et d'intégration. Comptez simplement sur des tests unitaires pour de petites unités de fonctionnalité et ne répétez aucune logique qui est déjà dans un test unitaire dans un test d'intégration. Cela entraînera souvent moins de travail (et donc moins de reprise).

Zachary Yates
la source
4

Les tests sont utiles lorsqu'ils se cassent: de manière proactive ou réactive.

Les tests unitaires sont proactifs et peuvent être une vérification fonctionnelle continue tandis que les tests d'intégration sont réactifs sur des données par étapes / fausses.

Si le système considéré est plus proche / dépend des données que de la logique de calcul, les tests d'intégration devraient avoir plus d'importance.

Par exemple, si nous construisons un système ETL, les tests les plus pertinents porteront sur les données (mises en scène, truquées ou en direct). Le même système aura quelques tests unitaires, mais uniquement autour de la validation, du filtrage, etc.

Le modèle de référentiel ne doit pas avoir de logique de calcul ou métier. Il est très proche de la base de données. Mais le référentiel est également proche de la logique métier qui l'utilise. Il s'agit donc de trouver un ÉQUILIBRE.

Les tests unitaires peuvent tester le comportement du référentiel. Les tests d'intégration peuvent tester CE QUI EST VRAIMENT ARRIVÉ lors de l'appel du référentiel.

Maintenant, "CE QUI EST VRAIMENT ARRIVÉ" peut sembler très attrayant. Mais cela pourrait être très coûteux pour une exécution cohérente et répétée. La définition classique des tests de fragilité s'applique ici.

Donc, la plupart du temps, je trouve juste assez bon pour écrire un test unitaire sur un objet qui utilise le référentiel. De cette façon, nous testons si la méthode de dépôt appropriée a été appelée et si les résultats Mocked appropriés sont renvoyés.

Otpidus
la source
J'aime ceci: "Maintenant," CE QUI A VRAIMENT ARRIVÉ "peut sembler très attrayant. Mais cela pourrait être très coûteux pour une exécution cohérente et répétée."
atconway
2

Il y a 3 choses distinctes qui doivent être testées: la procédure stockée, le code qui appelle la procédure stockée (c'est-à-dire votre classe de référentiel) et le consommateur. Le travail du référentiel consiste à générer une requête et à convertir l'ensemble de données renvoyé en objet de domaine. Il y a suffisamment de code pour prendre en charge les tests unitaires qui se distinguent de l'exécution réelle de la requête et de la création de l'ensemble de données.

Donc (ceci est un exemple très très simplifié):

interface IOrderRepository
{
    Order GetOrderByID(Guid id);
}

class OrderRepository : IOrderRepository
{
    private readonly ISqlExecutor sqlExecutor;
    public OrderRepository(ISqlExecutor sqlExecutor)
    {
        this.sqlExecutor = sqlExecutor;
    }

    public Order GetOrderByID(Guid id)
    {
        var sql = "SELECT blah, blah FROM Order WHERE OrderId = @p0";
        var dataset = this.sqlExecutor.Execute(sql, p0 = id);
        var result = this.orderFromDataset(dataset);
        return result;
    }
}

Ensuite, lors du test OrderRepository, passez un simulé ISqlExecutoret vérifiez que l'objet sous test réussit dans le SQL correct (c'est son travail) et retourne un Orderobjet approprié étant donné un ensemble de données de résultat (également simulé). La seule façon de tester la SqlExecutorclasse concrète est probablement avec des tests d'intégration, c'est juste, mais c'est une classe à enveloppe mince et elle changera rarement, donc c'est très important.

Vous devez également tester également vos procédures stockées.

Scott Whitlock
la source
0

Je peux réduire cela à une ligne de pensée très simple: favoriser les tests unitaires car cela aide à localiser les bogues. Et cela de manière cohérente car les incohérences provoquent de la confusion.

D'autres raisons découlent des mêmes règles qui guident le développement OO puisqu'elles s'appliquent également aux tests. Par exemple, le principe de responsabilité unique.

Si certains tests unitaires semblent ne pas valoir la peine, alors c'est peut-être un signe que le sujet ne vaut vraiment pas la peine. Ou peut-être que sa fonctionnalité est plus abstraite que son homologue, et les tests pour elle (ainsi que son code) peuvent être abstraits à un niveau supérieur.

Comme pour tout, il y a des exceptions. Et la programmation est encore un peu une forme d'art, donc de nombreux problèmes sont suffisamment différents pour justifier l'évaluation de chacun pour la meilleure approche.

CL22
la source