Je travaille sur un comparateur de liste pour aider à trier une liste non ordonnée de résultats de recherche selon les exigences très spécifiques de notre client. Les exigences nécessitent un algorithme de pertinence classé avec les règles suivantes par ordre d'importance:
- Correspondance exacte sur le nom
- Tous les mots de la requête de recherche dans le nom ou un synonyme du résultat
- Quelques mots de la requête de recherche dans le nom ou le synonyme du résultat (% décroissant)
- Tous les mots de la requête de recherche dans la description
- Quelques mots de la requête de recherche dans la description (% décroissant)
- Dernière date de modification décroissante
Le choix de conception naturel pour ce comparateur semblait être un classement noté basé sur des puissances de 2. La somme des règles moins importantes ne peut jamais être plus qu'une correspondance positive sur une règle de plus grande importance. Ceci est réalisé par le score suivant:
- 32
- 16
- 8 (score bris d'égalité secondaire basé sur le% décroissant)
- 4
- 2 (score bris d'égalité secondaire basé sur le% décroissant)
- 1
Dans l'esprit TDD, j'ai décidé de commencer par mes tests unitaires en premier. Avoir un scénario de test pour chaque scénario unique serait au minimum de 63 scénarios de test uniques sans tenir compte des scénarios de test supplémentaires pour la logique de bris d'égalité secondaire sur les règles 3 et 5. Cela semble dominateur.
Les tests réels seront en fait moins importants. Sur la base des règles réelles elles-mêmes, certaines règles garantissent que les règles inférieures seront toujours vraies (par exemple, lorsque «Tous les mots de la requête de recherche apparaissent dans la description», la règle «Certains mots de la requête de recherche apparaissent dans la description» sera toujours vraie). Le niveau d'effort pour écrire chacun de ces cas de test en vaut-il la peine? S'agit-il du niveau de test généralement requis lorsque l'on parle de couverture de test à 100% en TDD? Sinon, quelle serait une stratégie de test alternative acceptable?
la source
Réponses:
Votre question implique que TDD a quelque chose à voir avec "l'écriture de tous les cas de test en premier". À mon humble avis, ce n'est pas «dans l'esprit du TDD», c'est en fait contre . N'oubliez pas que TDD est synonyme de « développement piloté par les tests », vous n'avez donc besoin que des cas de test qui «pilotent» réellement votre implémentation, pas plus. Et tant que votre implémentation n'est pas conçue de manière à ce que le nombre de blocs de code augmente de façon exponentielle à chaque nouvelle exigence, vous n'aurez pas besoin non plus d'un nombre exponentiel de cas de test. Dans votre exemple, le cycle TDD ressemblera probablement à ceci:
Ensuite, commencez par la 2e exigence:
Voici le problème : lorsque vous ajoutez des cas de test pour l'exigence / le numéro de catégorie "n", vous n'aurez qu'à ajouter des tests pour vous assurer que le score de la catégorie "n-1" est supérieur à celui de la catégorie "n" . Vous n'aurez pas à ajouter de cas de test pour chaque autre combinaison des catégories 1, ..., n-1, car les tests que vous avez écrits auparavant vous assureront que les scores de ces catégories seront toujours dans le bon ordre.
Cela vous donnera donc un certain nombre de cas de test qui croît approximativement de façon linéaire avec le nombre d'exigences, et non de façon exponentielle.
la source
Envisagez d'écrire une classe qui passe par une liste prédéfinie de conditions et multiplie un score actuel par 2 pour chaque vérification réussie.
Cela peut être testé très facilement, en utilisant seulement quelques tests simulés.
Ensuite, vous pouvez écrire une classe pour chaque condition et il n'y a que 2 tests pour chaque cas.
Je ne comprends pas vraiment votre cas d'utilisation, mais j'espère que cet exemple vous aidera.
Vous remarquerez que vos tests de 2 ^ conditions se résument rapidement à 4+ (2 * conditions). 20 est beaucoup moins autoritaire que 64. Et si vous en ajoutez une autre plus tard, vous n'avez pas à changer TOUTES les classes existantes (principe ouvert-fermé), donc vous n'avez pas à écrire 64 nouveaux tests, vous avez juste pour ajouter une autre classe avec 2 nouveaux tests et l'injecter dans votre classe ScoreBuilder.
la source
Vous devrez définir "ça vaut le coup". Le problème avec ce type de scénario est que les tests auront un retour sur l'utilité décroissant. Le premier test que vous écrivez en vaudra certainement la peine. Il peut trouver des erreurs évidentes dans la priorité et même des choses comme l'analyse des erreurs lors de la tentative de décomposition des mots.
Le deuxième test en vaudra la peine car il couvre un chemin différent à travers le code, vérifiant probablement une autre relation de priorité.
Le 63e test n'en vaudra probablement pas la peine car c'est quelque chose dont vous êtes sûr à 99,99% qu'il est couvert par la logique de votre code ou d'un autre test.
Ma compréhension est que la couverture à 100% signifie que tous les chemins de code sont exercés. Cela ne signifie pas que vous effectuez toutes les combinaisons de vos règles, mais tous les différents chemins que votre code pourrait emprunter (comme vous le faites remarquer, certaines combinaisons ne peuvent pas exister dans le code). Mais puisque vous faites du TDD, il n'y a pas encore de "code" pour vérifier les chemins. La lettre du processus dirait faire tous les 63+.
Personnellement, je trouve que la couverture à 100% est un rêve de pipe. Au-delà, c'est peu pragmatique. Les tests unitaires existent pour vous servir, et non l'inverse. À mesure que vous effectuez plus de tests, vous obtenez des rendements décroissants sur l'avantage (la probabilité que le test empêche un bogue + la confiance que le code est correct). Selon ce que fait votre code, vous définissez où sur cette échelle mobile vous arrêtez de faire des tests. Si votre code exécute un réacteur nucléaire, alors peut-être que tous les tests 63+ en valent la peine. Si votre code organise vos archives musicales, vous pourriez probablement vous en sortir avec beaucoup moins.
la source
Je dirais que c'est un cas parfait pour TDD.
Vous disposez d'un ensemble connu de critères à tester, avec une répartition logique de ces cas. En supposant que vous allez les tester unitairement maintenant ou plus tard, il semble logique de prendre le résultat connu et de le construire en vous assurant en fait de couvrir chacune des règles de manière indépendante.
De plus, vous pouvez découvrir au fur et à mesure si l'ajout d'une nouvelle règle de recherche rompt une règle existante. Si vous faites tout cela à la fin du codage, vous courez probablement un plus grand risque de devoir en changer un pour en corriger un, qui en casse un autre, qui en casse un autre ... Et, vous apprenez en mettant en œuvre les règles si votre conception est valide ou a besoin de peaufiner.
la source
Je ne suis pas un fan de l'interprétation stricte d'une couverture de test à 100% comme de la rédaction de spécifications par rapport à chaque méthode ou du test de chaque permutation du code. Faire cela de manière fanatique a tendance à conduire à une conception pilotée par les tests de vos classes qui n'encapsule pas correctement la logique métier et génère des tests / spécifications qui n'ont généralement aucun sens en termes de description de la logique métier prise en charge. Au lieu de cela, je me concentre sur la structuration des tests un peu comme les règles métier elles-mêmes et je m'efforce d'exercer chaque branche conditionnelle du code avec des tests dans l'espoir explicite que ces tests soient facilement compris par le testeur comme des cas d'utilisation généraux et décrivent en fait le règles commerciales qui ont été mises en œuvre.
Avec cette idée en tête, je testerais de manière exhaustive les 6 facteurs de classement que vous avez énumérés isolément, suivi de 2 ou 3 tests de style d'intégration qui garantissent que vous obtenez vos résultats aux valeurs de classement globales attendues. Par exemple, le cas # 1, Exact Match on Name, j'aurais au moins deux tests unitaires pour tester quand c'est exact et quand ce n'est pas le cas et que les deux scénarios renvoient le score attendu. S'il est sensible à la casse, un cas pour tester "Exact Match" par rapport à "exact match" et éventuellement d'autres variations d'entrée telles que la ponctuation, des espaces supplémentaires, etc. renvoie également les scores attendus.
Une fois que j'ai étudié tous les facteurs individuels contribuant aux scores de classement, je suppose essentiellement que ceux-ci fonctionnent correctement au niveau de l'intégration et je me concentre sur la garantie que leurs facteurs combinés contribuent correctement au score de classement final attendu.
En supposant que les cas # 2 / # 3 et # 4 / # 5 sont généralisés aux mêmes méthodes sous-jacentes, mais en passant des champs différents, vous n'avez qu'à écrire un ensemble de tests unitaires pour les méthodes sous-jacentes et écrire de simples tests unitaires supplémentaires pour tester le spécifique (titre, nom, description, etc.) et la notation dans l'affacturage désigné, ce qui réduit encore la redondance de votre effort de test global.
Avec cette approche, l'approche décrite ci-dessus produirait probablement 3 ou 4 tests unitaires sur le cas # 1, peut-être 10 spécifications sur certains / tous avec des synonymes pris en compte - plus 4 spécifications sur la notation correcte des cas # 2 - # 5 et 2 à 3 spécifications sur le classement de la date finale ordonnée, puis 3 à 4 tests de niveau d'intégration qui mesurent les 6 cas combinés de manière probable (oubliez les cas de bord obscurs pour l'instant, sauf si vous voyez clairement un problème dans votre code qui doit être exercé pour garantir cette condition est gérée) ou assurez - vous de ne pas être violé / cassé par des révisions ultérieures. Cela donne environ 25 spécifications pour exercer 100% du code écrit (même si vous n'avez pas appelé directement 100% des méthodes écrites).
la source
Je n'ai jamais été fan d'une couverture de test à 100%. D'après mon expérience, si quelque chose est assez simple à tester avec seulement un ou deux cas de test, alors c'est assez simple pour échouer rarement. Quand il échoue, c'est généralement en raison de modifications architecturales qui nécessiteraient de toute façon des modifications de test.
Cela étant dit, pour des exigences comme la vôtre, je fais toujours des tests unitaires minutieux, même sur des projets personnels où personne ne me fait, car ce sont les cas où les tests unitaires vous font gagner du temps et vous aggravent. Plus il y aura de tests unitaires requis pour tester quelque chose, plus les tests unitaires de temps permettront d'économiser.
C'est parce que vous ne pouvez tenir que tant de choses dans votre tête à la fois. Si vous essayez d'écrire du code qui fonctionne pour 63 combinaisons différentes, il est souvent difficile de corriger une combinaison sans en casser une autre. Vous finissez par tester manuellement d'autres combinaisons encore et encore. Les tests manuels sont beaucoup plus lents, ce qui vous empêche de réexécuter toutes les combinaisons possibles chaque fois que vous effectuez un changement. Cela vous rend plus susceptible de manquer quelque chose et plus de chances de perdre du temps à suivre des chemins qui ne fonctionnent pas dans tous les cas.
Outre le gain de temps par rapport aux tests manuels, il y a beaucoup moins de tension mentale, ce qui facilite la concentration sur le problème à résoudre sans se soucier d'introduire accidentellement des régressions. Cela vous permet de travailler plus rapidement et plus longtemps sans épuisement. À mon avis, les avantages pour la santé mentale valent à eux seuls le coût des tests unitaires de code complexe, même si cela ne vous a pas fait gagner de temps.
la source