Ich arbeite an einem Listenkomparator, um die Sortierung einer ungeordneten Liste von Suchergebnissen nach ganz bestimmten Anforderungen unseres Kunden zu unterstützen. Die Anforderungen verlangen nach einem eingestuften Relevanzalgorithmus mit den folgenden Regeln in der Reihenfolge der Wichtigkeit:
- Genaue Übereinstimmung mit dem Namen
- Alle Wörter der Suche werden im Namen oder einem Synonym des Ergebnisses abgefragt
- Einige Wörter der Suchanfrage im Namen oder Synonym des Ergebnisses (% absteigend)
- Alle Wörter der Suchanfrage in der Beschreibung
- Einige Wörter der Suchanfrage in der Beschreibung (% absteigend)
- Datum der letzten Änderung absteigend
Die natürliche Designwahl für diesen Komparator schien eine auf Potenzen von 2 basierende Wertung zu sein. Die Summe von weniger wichtigen Regeln kann niemals mehr als eine positive Übereinstimmung mit einer Regel mit höherer Wichtigkeit sein. Dies wird durch die folgende Punktzahl erreicht:
- 32
- 16
- 8 (Sekundäre Tie-Breaker-Punktzahl basierend auf% absteigend)
- 4
- 2 (Sekundäre Tie-Breaker-Punktzahl basierend auf% absteigend)
- 1
Im Sinne des TDD habe ich mich dazu entschlossen, zuerst mit meinen Unit-Tests zu beginnen. Einen Testfall für jedes einzelne Szenario zu haben, würde mindestens 63 einzelnen Testfällen entsprechen, wobei zusätzliche Testfälle für die Logik der sekundären Verbindungsunterbrecher in den Regeln 3 und 5 nicht berücksichtigt werden. Dies scheint überheblich.
Die tatsächlichen Tests werden jedoch tatsächlich weniger sein. Auf der Grundlage der eigentlichen Regeln stellen bestimmte Regeln sicher, dass niedrigere Regeln immer zutreffen (z. B. Wenn "Alle Suchbegriffe in der Beschreibung enthalten", ist die Regel "Einige Suchbegriffe in der Beschreibung enthalten" immer zutreffend). Lohnt sich der Aufwand, jeden dieser Testfälle zu schreiben? Ist dies die Teststufe, die normalerweise erforderlich ist, wenn es um eine Testabdeckung von 100% bei TDD geht? Wenn nicht, was wäre dann eine akzeptable alternative Teststrategie?
quelle
Antworten:
Ihre Frage impliziert, dass TDD etwas mit "Schreiben aller Testfälle zuerst" zu tun hat. IMHO ist das nicht "im Geiste von TDD", eigentlich ist es dagegen . Denken Sie daran, dass TDD für "Test Driven Development" steht. Sie benötigen also nur die Testfälle, die Ihre Implementierung wirklich "vorantreiben", nicht mehr. Und solange Ihre Implementierung nicht so konzipiert ist, dass die Anzahl der Codeblöcke mit jeder neuen Anforderung exponentiell wächst, benötigen Sie auch keine exponentielle Anzahl von Testfällen. In Ihrem Beispiel sieht der TDD-Zyklus wahrscheinlich so aus:
Beginnen Sie dann mit der 2. Anforderung:
Hier ist der Haken : Wenn Sie Testfälle für die Anforderungs- / Kategorienummer "n" hinzufügen, müssen Sie nur Tests hinzufügen, um sicherzustellen, dass die Punktzahl der Kategorie "n-1" höher ist als die Punktzahl für die Kategorie "n". . Sie müssen keine Testfälle für jede andere Kombination der Kategorien 1, ..., n-1 hinzufügen, da die von Ihnen zuvor geschriebenen Tests sicherstellen, dass die Ergebnisse dieser Kategorien weiterhin in der richtigen Reihenfolge vorliegen.
So erhalten Sie eine Reihe von Testfällen, die nicht exponentiell, sondern ungefähr linear mit der Anzahl der Anforderungen wachsen.
quelle
Schreiben Sie eine Klasse, die eine vordefinierte Liste von Bedingungen durchläuft und für jede erfolgreiche Prüfung eine aktuelle Punktzahl mit 2 multipliziert.
Dies kann sehr einfach mit nur ein paar verspotteten Tests getestet werden.
Dann können Sie für jede Bedingung eine Klasse schreiben und es gibt nur 2 Tests für jeden Fall.
Ich verstehe Ihren Anwendungsfall nicht wirklich, aber hoffentlich hilft dieses Beispiel.
Sie werden feststellen, dass Ihre 2 ^ -Bedingungenstests schnell 4+ (2 * -Bedingungen) ergeben. 20 ist viel weniger anstrengend als 64. Und wenn Sie später eine weitere hinzufügen, müssen Sie KEINE der vorhandenen Klassen ändern (Open-Closed-Prinzip), sodass Sie keine 64 neuen Tests schreiben müssen, sondern nur um eine weitere Klasse mit 2 neuen Tests hinzuzufügen und diese in Ihre ScoreBuilder-Klasse einzufügen.
quelle
Sie müssen "wert" definieren. Das Problem bei dieser Art von Szenario ist, dass die Tests eine abnehmende Rendite für die Nützlichkeit haben. Der erste Test, den Sie schreiben, wird sich auf jeden Fall lohnen. Es kann offensichtliche Fehler in der Priorität und sogar Dinge wie Parsing-Fehler finden, wenn versucht wird, die Wörter aufzubrechen.
Der zweite Test lohnt sich, da er einen anderen Pfad durch den Code abdeckt und möglicherweise eine andere Prioritätsbeziehung überprüft.
Der 63. Test wird sich wahrscheinlich nicht lohnen, da Sie zu 99,99% sicher sind, dass er von der Logik Ihres Codes oder eines anderen Tests abgedeckt wird.
Mein Verständnis ist 100% Abdeckung bedeutet, dass alle Codepfade ausgeübt werden. Dies bedeutet nicht, dass Sie alle Kombinationen Ihrer Regeln ausführen, aber alle unterschiedlichen Pfade, auf denen Ihr Code ausgeführt werden kann (wie Sie bereits betont haben, können einige Kombinationen im Code nicht vorhanden sein). Da Sie jedoch TDD ausführen, gibt es noch keinen "Code", nach dem Sie suchen müssen. Der Buchstabe des Prozesses würde sagen, alle 63+ zu machen.
Ich persönlich empfinde 100% Deckung als Wunschtraum. Darüber hinaus ist es unpragmatisch. Unit-Tests dienen Ihnen und nicht umgekehrt. Wenn Sie mehr Tests durchführen, erhalten Sie eine geringere Rendite für den Vorteil (die Wahrscheinlichkeit, dass der Test einen Fehler verhindert, + die Gewissheit, dass der Code korrekt ist). Abhängig davon, was Ihr Code tut, legen Sie fest, wo Sie auf dieser Skala keine Tests mehr durchführen. Wenn Ihr Code einen Kernreaktor betreibt, lohnen sich vielleicht alle 63+ Tests. Wenn Ihr Code Ihr Musikarchiv organisiert, könnten Sie wahrscheinlich mit viel weniger davonkommen.
quelle
Ich würde argumentieren, dass dies ein perfekter Fall für TDD ist.
Sie müssen eine Reihe bekannter Kriterien testen, mit einer logischen Aufschlüsselung dieser Fälle. Angenommen, Sie werden sie entweder jetzt oder später in einem Komponententest testen, erscheint es sinnvoll, das bekannte Ergebnis zu verwenden und darauf aufzubauen, um sicherzustellen, dass Sie tatsächlich jede der Regeln unabhängig abdecken.
Außerdem erfahren Sie, wenn Sie eine neue Suchregel hinzufügen, die gegen eine vorhandene Regel verstößt. Wenn Sie dies alles am Ende der Codierung tun, besteht vermutlich ein höheres Risiko, dass Sie eines ändern müssen, um eines zu reparieren, was ein anderes zerstört, was ein anderes zerstört ... Und Sie lernen, wie Sie die Regeln implementieren, ob Ihr Entwurf gültig ist oder muss optimiert werden.
quelle
Ich bin kein Fan davon, eine 100% ige Testabdeckung streng zu interpretieren, indem ich Spezifikationen für jede einzelne Methode schreibe oder jede Permutation des Codes teste. Wenn Sie dies fanatisch tun, führt dies in der Regel zu einem testgetriebenen Design Ihrer Klassen, das die Geschäftslogik nicht richtig einschließt und Tests / Spezifikationen liefert, die im Allgemeinen für die Beschreibung der unterstützten Geschäftslogik bedeutungslos sind. Stattdessen konzentriere ich mich auf die Strukturierung der Tests, ähnlich wie die Geschäftsregeln selbst, und bemühe mich, jeden bedingten Zweig des Codes mit Tests auszuüben, mit der ausdrücklichen Erwartung, dass die Tests für den Tester leicht zu verstehen sind, wie es die allgemeinen Anwendungsfälle wären und tatsächlich beschreiben Geschäftsregeln, die implementiert wurden.
Unter Berücksichtigung dieser Idee würde ich die 6 Ranking-Faktoren, die Sie für sich aufgelistet haben, ausführlich testen und anschließend zwei oder drei Integrationstests durchführen, um sicherzustellen, dass Sie Ihre Ergebnisse auf die erwarteten Gesamtranking-Werte bringen. Beispiel: In Fall 1, Exakte Übereinstimmung mit dem Namen, müssten mindestens zwei Komponententests durchgeführt werden, um zu testen, wann genau und wann nicht und ob die beiden Szenarien die erwartete Punktzahl zurückgeben. Wenn die Groß- und Kleinschreibung beachtet wird, gibt auch ein Testfall für "Exakte Übereinstimmung" im Vergleich zu "Exakte Übereinstimmung" und möglicherweise für andere Eingabevariationen wie Interpunktion, zusätzliche Leerzeichen usw. die erwarteten Ergebnisse zurück.
Nachdem ich alle einzelnen Faktoren durchgearbeitet habe, die zur Bewertung des Rankings beitragen, gehe ich im Wesentlichen davon aus, dass diese auf der Integrationsebene korrekt funktionieren, und konzentriere mich darauf, sicherzustellen, dass ihre kombinierten Faktoren korrekt zur endgültigen erwarteten Bewertung des Rankings beitragen.
Angenommen, die Fälle Nr. 2 / Nr. 3 und Nr. 4 / Nr. 5 werden auf die gleichen zugrunde liegenden Methoden verallgemeinert, aber Sie müssen nur einen Satz von Komponententests für die zugrunde liegenden Methoden schreiben und einfache zusätzliche Komponententests schreiben, um die spezifischen zu testen Felder (Titel, Name, Beschreibung usw.) und Bewertung beim Designated Factoring, sodass die Redundanz Ihres gesamten Testaufwands weiter reduziert wird.
Mit diesem Ansatz würde der oben beschriebene Ansatz wahrscheinlich 3 oder 4 Einheitentests für Fall Nr. 1 ergeben, möglicherweise 10 Spezifikationen für einige / alle mit Synonymen, plus 4 Spezifikationen für die korrekte Bewertung der Fälle Nr. 2 - Nr. 5 und 2 bis zu 3 Spezifikationen am Enddatum bestellt Rangfolge, dann 3 bis 4 Integrationstests, die alle 6 Fälle in wahrscheinlicher Weise kombiniert messen (vergessen Sie vorerst obskure Randfälle, es sei denn, Sie sehen eindeutig ein Problem in Ihrem Code, das ausgeübt werden muss, um dies sicherzustellen Diese Bedingung wird behandelt) oder stellen Sie sicher, dass durch spätere Überarbeitungen keine Verletzungen oder Brüche verursacht werden. Das ergibt ungefähr 25 Specs, um 100% des geschriebenen Codes auszuüben (obwohl Sie nicht direkt 100% der geschriebenen Methoden aufgerufen haben).
quelle
Ich war noch nie ein Fan von 100% Testabdeckung. Wenn etwas so einfach ist, dass es nur mit ein oder zwei Testfällen getestet werden kann, ist es meiner Erfahrung nach so einfach, dass es selten fehlschlägt. Wenn dies fehlschlägt, liegt dies normalerweise an Änderungen der Architektur, die ohnehin Teständerungen erfordern würden.
Abgesehen davon, für Anforderungen wie Ihre, teste ich meine Geräte immer gründlich, auch bei persönlichen Projekten, bei denen mich niemand dazu bringt, denn das erspart Ihnen Zeit und Ärger. Je mehr Unit-Tests erforderlich sind, um etwas zu testen, desto mehr Zeit sparen Unit-Tests.
Das liegt daran, dass Sie nur so viele Dinge gleichzeitig in Ihrem Kopf halten können. Wenn Sie versuchen, Code zu schreiben, der für 63 verschiedene Kombinationen funktioniert, ist es oft schwierig, eine Kombination zu reparieren, ohne eine andere zu beschädigen. Am Ende testen Sie manuell immer wieder andere Kombinationen. Das manuelle Testen ist viel langsamer, sodass Sie nicht bei jeder Änderung jede mögliche Kombination wiederholen möchten. Dies führt dazu, dass Sie mit größerer Wahrscheinlichkeit etwas verpassen und Zeit damit verschwenden, Pfade zu verfolgen, die nicht in allen Fällen funktionieren.
Abgesehen von der Zeitersparnis im Vergleich zu manuellen Tests ist die mentale Belastung erheblich geringer, sodass Sie sich leichter auf das jeweilige Problem konzentrieren können, ohne sich Gedanken über versehentliche Regressionen machen zu müssen. So können Sie schneller und länger ohne Burnout arbeiten. Meiner Meinung nach sind die Vorteile für die psychische Gesundheit allein die Kosten für das Testen komplexer Codes wert, auch wenn Sie dadurch keine Zeit gespart haben.
quelle