Wie vermeide ich logische Fehler im Code, wenn TDD nicht geholfen hat?

67

Ich habe kürzlich ein kleines Stück Code geschrieben, das auf menschenfreundliche Weise anzeigt, wie alt ein Ereignis ist. Beispielsweise könnte dies darauf hinweisen, dass das Ereignis „vor drei Wochen“ oder „vor einem Monat“ oder „gestern“ stattgefunden hat.

Die Anforderungen waren relativ klar und dies war ein perfekter Fall für eine testgetriebene Entwicklung. Ich schrieb die Tests nacheinander und implementierte den Code, um jeden Test zu bestehen, und alles schien perfekt zu funktionieren. Bis ein Fehler in der Produktion auftauchte.

Hier ist der relevante Code:

now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
    return "Today"

yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
    return "Yesterday"

delta = (now - event_date).days

if delta < 7:
    return _number_to_text(delta) + " days ago"

if delta < 30:
    weeks = math.floor(delta / 7)
    if weeks == 1:
        return "A week ago"

    return _number_to_text(weeks) + " weeks ago"

if delta < 365:
    ... # Handle months and years in similar manner.

Die Tests überprüften den Fall eines Ereignisses, das heute, gestern, vor vier Tagen, vor zwei Wochen, vor einer Woche usw. stattfand, und der Code wurde entsprechend erstellt.

Was ich vermisst habe, ist, dass ein Ereignis vorgestern stattfinden kann, während es vorgestern war: Zum Beispiel wäre ein Ereignis vor sechsundzwanzig Stunden vorgestern, während es nicht genau gestern war, wenn es jetzt 1 Uhr morgens ist. Genauer gesagt, es ist ein Punkt etwas, aber da das deltaeine ganze Zahl ist, wird es nur eine sein. In diesem Fall zeigt die Anwendung "Vor einem Tag" an. Dies ist offensichtlich unerwartet und wird im Code nicht behandelt. Es kann behoben werden, indem Folgendes hinzugefügt wird:

if delta == 1:
    return "A day ago"

kurz nach der Berechnung der delta.

Die einzige negative Konsequenz des Fehlers ist, dass ich eine halbe Stunde damit verbracht habe, mich zu fragen, wie dieser Fall passieren könnte (und zu glauben, dass er trotz der einheitlichen Verwendung von UTC im Code mit Zeitzonen zu tun hat), aber seine Anwesenheit beunruhigt mich. Es zeigt an, dass:

  • Es ist sehr einfach, einen logischen Fehler zu begehen, selbst in einem solch einfachen Quellcode.
  • Testgetriebene Entwicklung hat nicht geholfen.

Ebenfalls besorgniserregend ist, dass ich nicht sehen kann, wie solche Fehler vermieden werden können. Abgesehen davon, dass ich mehr nachdenke, bevor ich Code schreibe, kann ich mir nur vorstellen, viele Asserts für die Fälle hinzuzufügen, von denen ich glaube, dass sie niemals auftreten würden (so wie ich dachte, dass ein Tag zuvor notwendigerweise gestern ist), und dann jede Sekunde für zu durchlaufen In den letzten zehn Jahren wurde nach Behauptungsverletzungen gesucht, die zu komplex erscheinen.

Wie könnte ich es vermeiden, diesen Bug überhaupt zu erzeugen?

Arseni Mourzenko
quelle
38
Indem Sie einen Testfall dafür haben? Das scheint so zu sein, wie Sie es später entdeckt haben, und passt zu TDD.
Urous
63
Sie haben gerade erfahren, warum ich kein Fan von testgetriebener Entwicklung bin. Meiner Erfahrung nach sind die meisten Fehler in der Produktion Szenarien, an die niemand gedacht hat. Testgetriebene Entwicklung und Unit-Tests machen dafür nichts. (Unit-Tests sind jedoch
nützlich,
102
Wiederholen Sie nach mir: "Es gibt keine Silberkugeln, einschließlich TDD." Es gibt keinen Prozess, keine Regeln, keinen Algorithmus, dem Sie automatisch folgen können, um perfekten Code zu erstellen. Wäre dies der Fall, könnten wir den gesamten Prozess automatisieren und damit fertig werden.
Jpmc26
43
Herzlichen Glückwunsch, Sie haben die alte Weisheit wiederentdeckt, dass keine Tests das Fehlen von Fehlern beweisen können. Wenn Sie jedoch nach Techniken suchen, mit denen die mögliche Eingabedomäne besser abgedeckt werden kann, müssen Sie eine gründliche Analyse der Domäne, der Randfälle und der Äquivalenzklassen dieser Domäne durchführen. Alle alten, bekannten Techniken, die lange vor dem Begriff TDD bekannt waren, wurden erfunden.
Doc Brown
80
Ich versuche nicht, scharf zu sein, aber Ihre Frage könnte anscheinend wie folgt umformuliert werden: "Wie denke ich an Dinge, an die ich nicht gedacht habe?". Nicht sicher, was das mit TDD zu tun hat.
Jared Smith

Antworten:

57

Dies sind die Arten von Fehlern, die Sie normalerweise im Refactor- Schritt "Rot / Grün / Refactor" finden. Vergiss diesen Schritt nicht! Betrachten Sie einen Refaktor wie den folgenden (ungetesteten):

def pluralize(num, unit):
    if num == 1:
        return unit
    else:
        return unit + "s"

def convert_to_unit(delta, unit):
    factor = 1
    if unit == "week":
        factor = 7 
    elif unit == "month":
        factor = 30
    elif unit == "year":
        factor = 365
    return delta // factor

def best_unit(delta):
    if delta < 7:
        return "day"
    elif delta < 30:
        return "week"
    elif delta < 365:
        return "month"
    else:
        return "year"

def human_friendly(event_date):
    date = event_date.date()
    today = now.date()
    yesterday = today - datetime.timedelta(1)
    if date == today:
        return "Today"
    elif date == yesterday:
        return "Yesterday"
    else:
        delta = (now - event_date).days
        unit = best_unit(delta)
        converted = convert_to_unit(delta, unit)
        pluralized = pluralize(converted, unit)
        return "{} {} ago".format(converted, pluralized)

Hier haben Sie 3 Funktionen auf einer niedrigeren Abstraktionsebene erstellt, die viel zusammenhängender sind und isoliert leichter getestet werden können. Wenn Sie eine Zeitspanne, die Sie beabsichtigt hatten, weglassen, würde sie in den einfacheren Hilfsfunktionen wie ein schmerzender Daumen herausragen. Durch das Entfernen von Duplikaten verringern Sie außerdem das Fehlerpotenzial. Sie müssten tatsächlich Code hinzufügen , um Ihren kaputten Fall zu implementieren.

Bei einer überarbeiteten Form wie dieser fallen auch andere subtilere Testfälle leichter ein. Was soll best_unitman zum Beispiel tun, wenn deltaes negativ ist?

Mit anderen Worten, Refactoring ist nicht nur dazu da, es hübsch zu machen. Dies erleichtert es dem Menschen, Fehler zu erkennen, die der Compiler nicht erkennen kann.

Karl Bielefeldt
quelle
12
Der nächste Schritt ist die Internationalisierung. pluralizeNur für eine Untergruppe von englischen Wörtern zu arbeiten, ist eine Verpflichtung.
Deduplizierer
@Deduplicator sicher, aber je nachdem, auf welche Sprachen / Kulturen Sie abzielen, müssen Sie möglicherweise nur noch pluralizemit numund uniteinen Schlüssel erstellen, um eine Formatzeichenfolge aus einer Tabelle / Ressourcendatei zu ziehen. ODER Sie müssen möglicherweise die Logik komplett umschreiben, da Sie andere Einheiten benötigen ;-)
Hulk
4
Ein Problem bleibt auch bei dieser Umgestaltung, die darin besteht, dass "gestern" in den frühen Morgenstunden (kurz nach 00:01 Uhr) nicht viel Sinn macht. Aus menschlicher Sicht ändert sich etwas, das um 23:59 Uhr geschah, nicht plötzlich von "heute" zu "gestern", wenn die Uhr nach Mitternacht abläuft. Es ändert sich stattdessen von "vor 1 Minute" zu "vor 2 Minuten". "Heute" ist zu grob in Bezug auf etwas, das vor wenigen Minuten passiert ist, und "gestern" ist voller Probleme für Nachtschwärmer.
David Hammen
@DavidHammen Dies ist ein Usability-Problem, das davon abhängt, wie genau Sie sein müssen. Wenn Sie es zumindest auf die Stunde genau wissen wollen, würde ich nicht denken, dass "gestern" gut ist. "Vor 24 Stunden" ist viel klarer und ein häufig verwendeter menschlicher Ausdruck, um die Anzahl der Stunden hervorzuheben. Computer, die versuchen, "menschlich freundlich" zu sein, verstehen dies fast immer falsch und verallgemeinern es auf "gestern", was zu vage ist. Aber um dies zu wissen, müssen Sie Benutzer interviewen, um zu sehen, was sie denken. Für einige Dinge möchten Sie wirklich das genaue Datum und die Uhrzeit, so dass "gestern" immer falsch ist.
Brandin
149

Testgetriebene Entwicklung hat nicht geholfen.

Es scheint, als hätte es geholfen, es ist nur so, dass Sie keinen Test für das Szenario "vor einem Tag" hatten. Vermutlich haben Sie einen Test hinzugefügt, nachdem dieser Fall gefunden wurde. Dies ist immer noch TDD, da Sie, wenn Fehler gefunden werden, einen Komponententest schreiben, um den Fehler zu erkennen, und ihn dann beheben.

Wenn Sie vergessen, einen Test für ein Verhalten zu schreiben, hilft Ihnen TDD nicht weiter. Sie vergessen, den Test zu schreiben und schreiben daher nicht die Implementierung.

esoterik
quelle
2
Alles könnte gesagt werden, wenn der Entwickler tdd nicht verwendet hätte, wäre es viel wahrscheinlicher, dass er auch andere Fälle übersehen hätte.
Caleb
75
Und denken Sie darüber hinaus darüber nach, wie viel Zeit gespart wurde, als der Fehler behoben wurde? Durch die vorhandenen Tests wussten sie sofort, dass ihre Änderung das vorhandene Verhalten nicht beeinträchtigt. Und sie konnten die neuen Testfälle und den Refactor hinzufügen, ohne anschließend umfangreiche manuelle Tests durchführen zu müssen.
Caleb
15
TDD ist nur so gut wie die Tests geschrieben.
Mindwin
Eine weitere Beobachtung: Das Hinzufügen des Tests für diesen Fall wird das Design verbessern, indem wir gezwungen werden, dies datetime.utcnow()aus der Funktion zu entfernen und stattdessen nowals (reproduzierbares) Argument zu übergeben.
Toby Speight
114

Ein Ereignis, das vor 26 Stunden stattfand, war vor einem Tag

Tests helfen nicht viel, wenn ein Problem schlecht definiert ist. Sie mischen offensichtlich Kalendertage mit Tagen, die in Stunden gerechnet werden. Wenn Sie sich an Kalendertage halten, ist die Zeit vor 26 Stunden um 1 Uhr nicht gestern. Wenn Sie sich an die Stunden halten, werden die Stunden vor 26 Stunden unabhängig von der Uhrzeit auf den Tag vor 1 Stunde gerundet.

Kevin Krumwiede
quelle
45
Dies ist ein großartiger Punkt. Das Fehlen einer Anforderung bedeutet nicht zwangsläufig, dass Ihr Implementierungsprozess fehlgeschlagen ist. Dies bedeutet nur, dass die Anforderung nicht genau definiert wurde. (Oder Sie haben einfach einen menschlichen Fehler begangen, der von Zeit zu Zeit
auftritt.
Dies ist die Antwort, die ich machen wollte. Ich definiere die Spezifikation als "Wenn das Ereignis dieser Kalendertag ist, wird das Delta in Stunden angezeigt. Andernfalls wird das Delta nur anhand von Datumsangaben bestimmt." Das Testen von Stunden ist nur innerhalb eines Tages sinnvoll, wenn darüber hinaus eine Auflösung von Tagen vorgesehen ist.
Baldrickk
1
Diese Antwort gefällt mir, weil sie das eigentliche Problem aufzeigt: Zeitpunkte und Daten sind zwei verschiedene Größen. Sie sind verwandt, aber wenn man sie vergleicht, geht es sehr schnell nach Süden. In der Programmierung ist die Datums- und Zeitlogik eines der schwierigsten Dinge, die es zu korrigieren gilt. Ich mag es wirklich nicht, dass viele Datumsimplementierungen das Datum grundsätzlich als 0:00 Uhr speichern. Das sorgt für viel Verwirrung.
Pieter B
38

Das kannst du nicht. TDD ist großartig, um Sie vor möglichen Problemen zu schützen, die Ihnen bewusst sind. Es hilft nicht, wenn Sie auf Probleme stoßen, an die Sie noch nie gedacht haben. Am besten lassen Sie das System von einer anderen Person testen, die möglicherweise die Randfälle findet, die Sie nie in Betracht gezogen haben.

Zugehörige Informationen: Ist es möglich, bei umfangreicher Software den absoluten Null-Fehler-Status zu erreichen?

Ian Jacobs
quelle
2
Es ist immer eine gute Idee, Tests von einer anderen Person als dem Entwickler schreiben zu lassen. Das bedeutet, dass beide Parteien die gleiche Eingabebedingung übersehen müssen, damit der Fehler in die Produktion gelangt.
Michael Kay
35

Es gibt zwei Ansätze, die mir normalerweise helfen können.

Zuerst suche ich nach den Randfällen. Dies sind Orte, an denen sich das Verhalten ändert. In Ihrem Fall ändert sich das Verhalten an mehreren Punkten entlang der Folge positiver ganzzahliger Tage. Es gibt einen Kantenfall bei Null, bei Eins, bei Sieben usw. Ich würde dann Testfälle an und um die Kantenfälle schreiben. Ich hätte Testfälle an -1 Tagen, 0 Tagen, 1 Stunden, 23 Stunden, 24 Stunden, 25 Stunden, 6 Tagen, 7 Tagen, 8 Tagen usw.

Das zweite, wonach ich suchen würde, sind Verhaltensmuster. In Ihrer Logik haben Sie seit Wochen eine spezielle Behandlung für eine Woche. Sie haben wahrscheinlich eine ähnliche Logik in jedem Ihrer anderen Intervalle, die nicht angezeigt werden. Diese Logik ist jedoch tagelang nicht vorhanden. Ich würde das mit Argwohn betrachten, bis ich entweder nachweislich erklären könnte, warum dieser Fall anders ist, oder ich füge die Logik hinzu.

cbojar
quelle
9
Dies ist ein wirklich wichtiger Teil von TDD, der oft übersehen wird, und ich habe selten in Artikeln und Leitfäden darüber gesprochen. Es ist wirklich wichtig, Randfälle und Randbedingungen zu testen, da ich feststelle, dass das die Ursache für 90% der Bugs ist -eine Fehler, Über- und Unterlauf, letzter Tag des Monats, letzter Monat des Jahres, Schaltjahre usw. usw.
GoatInTheMachine
2
@GoatInTheMachine - und 90% dieser 90% Bugs gibt es um Sommerzeit-Übergänge ..... Hahaha
Caleb
1
Sie können die möglichen Eingaben zunächst in Äquivalenzklassen unterteilen und dann die Kantenfälle an den Klassengrenzen bestimmen. Das ist natürlich ein Aufwand, der größer sein kann als der Entwicklungsaufwand. Ob sich das lohnt, hängt davon ab, wie wichtig es ist, Software so fehlerfrei wie möglich zu liefern, wie spät es ist und wie viel Geld und Geduld Sie haben.
Peter - Reinstate Monica
2
Das ist die richtige Antwort. Viele Geschäftsregeln erfordern, dass Sie einen Wertebereich in Intervalle unterteilen, in denen Fälle unterschiedlich behandelt werden müssen.
abuzittin gillifirca
14

Sie können nicht logische Fehler abfangen , die mit TDD in Ihren Anforderungen vorhanden sind. Trotzdem hilft TDD. Immerhin haben Sie den Fehler gefunden und einen Testfall hinzugefügt. Grundsätzlich stellt TDD jedoch nur sicher, dass der Code Ihrem mentalen Modell entspricht. Wenn Ihr mentales Modell fehlerhaft ist, werden Testfälle diese nicht erfassen.

Denken Sie jedoch daran, dass bei der Behebung des Fehlers die Testfälle, die Sie bereits überprüft hatten, kein vorhandenes, funktionierendes Verhalten beeinträchtigten. Das ist sehr wichtig, es ist einfach, einen Fehler zu beheben, aber einen anderen einzuführen.

Um diese Fehler im Voraus zu finden, versuchen Sie normalerweise, auf Äquivalenzklassen basierende Testfälle zu verwenden. Nach diesem Prinzip würden Sie einen Fall aus jeder Äquivalenzklasse und dann alle Kantenfälle auswählen.

Sie würden ein Datum von heute, gestern, vor ein paar Tagen, genau vor einer Woche und vor mehreren Wochen als Beispiele für jede Äquivalenzklasse auswählen. Beim Testen auf Daten stellen Sie außerdem sicher, dass bei Ihren Tests nicht das Systemdatum, sondern ein vorab festgelegtes Datum zum Vergleich verwendet wird. Dies würde auch einige Randfälle hervorheben: Sie würden sicherstellen, dass Ihre Tests zu einer beliebigen Tageszeit ausgeführt werden. Sie würden sie direkt nach Mitternacht, direkt vor Mitternacht und sogar direkt um Mitternacht ausführen . Dies bedeutet, dass es für jeden Test vier Basiszeiten gibt, gegen die getestet wird.

Dann würden Sie systematisch Randfälle zu allen anderen Klassen hinzufügen. Sie haben den Test für heute. Fügen Sie also eine Zeit hinzu, kurz bevor und nachdem sich das Verhalten ändern sollte. Das gleiche für gestern. Das gleiche für vor einer Woche usw.

Wenn Sie alle Randfälle systematisch auflisten und Testfälle für sie aufschreiben, stellen Sie möglicherweise fest, dass Ihre Spezifikation nicht detailliert genug ist, und fügen Sie sie hinzu. Beachten Sie, dass beim Umgang mit Daten häufig Fehler auftreten, da Benutzer häufig vergessen, ihre Tests zu schreiben, damit sie zu unterschiedlichen Zeiten ausgeführt werden können.

Beachten Sie jedoch, dass das meiste, was ich geschrieben habe, wenig mit TDD zu tun hat. Es geht darum, Äquivalenzklassen aufzuschreiben und sicherzustellen, dass Ihre eigenen Spezifikationen detailliert genug sind. Das ist der Prozess, mit dem Sie logische Fehler minimieren. TDD stellt nur sicher, dass Ihr Code Ihrem mentalen Modell entspricht.

Es ist schwierig, sich Testfälle auszudenken . Äquivalenzklassenbasiertes Testen ist noch nicht das Ende und kann in einigen Fällen die Anzahl der Testfälle erheblich erhöhen. In der Praxis ist das Hinzufügen all dieser Tests häufig wirtschaftlich nicht sinnvoll (auch wenn dies theoretisch erforderlich ist).

Polygnom
quelle
12

Die einzige Möglichkeit, die mir in den Sinn kommt, besteht darin, viele Aussagen für die Fälle hinzuzufügen, von denen ich glaube, dass sie niemals auftreten würden (so wie ich angenommen habe, dass ein Tag zuvor notwendigerweise gestern ist), und dann in den letzten zehn Jahren jede Sekunde durchzugehen und zu prüfen, ob sie eintreten jede Behauptungsverletzung, die zu komplex erscheint.

Warum nicht? Das klingt nach einer ziemlich guten Idee!

Das Hinzufügen von Verträgen (Behauptungen) zum Code ist eine ziemlich solide Methode, um dessen Korrektheit zu verbessern. Im Allgemeinen fügen wir sie als Vorbedingungen für den Funktionseintrag und als Nachbedingungen für die Funktionsrückgabe hinzu. Beispielsweise könnten wir eine Nachbedingung hinzufügen, dass alle zurückgegebenen Werte entweder die Form "Vor einer [Einheit]" oder "Vor einer [Zahl] [Einheit]" haben. Diszipliniert ausgeführt, führt dies zu einer vertraglichen Gestaltung und ist eine der häufigsten Methoden zum Schreiben von Code mit hoher Sicherheit.

Kritisch ist, dass die Verträge nicht zum Testen vorgesehen sind. Dies sind genauso viele Spezifikationen Ihres Codes wie Ihre Tests. Sie können jedoch über die Verträge testen : Rufen Sie den Code in Ihrem Test auf, und wenn keiner der Verträge Fehler auslöst, besteht der Test. In den letzten zehn Jahren jede Sekunde durchzuschleifen ist ein bisschen viel. Wir können jedoch einen anderen Teststil nutzen, der als eigenschaftsbasiertes Testen bezeichnet wird .

Anstatt in PBT auf bestimmte Ausgaben des Codes zu testen, testen Sie, ob die Ausgabe einer Eigenschaft entspricht. Zum Beispiel ist eine Eigenschaft eines reverse()ist Funktion , dass für jede Liste l, reverse(reverse(l)) = l. Der Vorteil solcher Schreibtests ist, dass die PBT-Engine einige hundert beliebige Listen (und einige pathologische Listen) erstellen und überprüfen kann, ob alle diese Eigenschaften aufweisen. Wenn dies nicht der Fall ist, "verkleinert" die Engine den fehlgeschlagenen Fall, um eine minimale Liste zu finden, die Ihren Code beschädigt. Es sieht so aus, als würden Sie Python schreiben, für das Hypothesis das wichtigste PBT-Framework ist.

Wenn Sie also einen guten Weg finden möchten, um schwierigere Randfälle zu finden, an die Sie vielleicht nicht denken, hilft es sehr, Verträge und immobilienbasierte Tests zusammen zu verwenden. Das ersetzt natürlich nicht das Schreiben von Unit-Tests, aber es erweitert es, was wirklich das Beste ist, was wir als Ingenieure tun können.

Hovercouch
quelle
2
Dies ist genau die richtige Lösung für diese Art von Problem. Die Menge der gültigen Ausgaben ist einfach zu definieren (Sie können sehr einfach einen regulären Ausdruck angeben, wie z. B. /(today)|(yesterday)|([2-6] days ago)|...). Anschließend können Sie den Prozess mit zufällig ausgewählten Eingaben ausführen, bis Sie eine finden, die nicht in der Menge der erwarteten Ausgaben enthalten ist. Diesen Ansatz würde diesen Fehler gefangen haben, und würde nicht zu realisieren erfordert , dass der Fehler vorher existieren könnte.
Jules
@Jules Siehe auch Eigenschaftsprüfung / -prüfung . Normalerweise schreibe ich während der Entwicklung Eigenschaftstests, um so viele unvorhergesehene Fälle wie möglich abzudecken, und zwinge mich, an allgemeine Eigenschaften / Invarianten zu denken. Ich speichere einmalige Tests für Regressionen und solche (von denen die Ausgabe des Autors eine Instanz ist)
Warbo
1
Wenn Sie so viele Schleifen in Tests durchführen, dauert dies sehr lange, was eines der Hauptziele von Unit-Tests zunichte macht: Führen Sie die Tests schnell durch !
CJ Dennis
5

Dies ist ein Beispiel, in dem das Hinzufügen von Modularität hilfreich gewesen wäre. Wenn ein fehleranfälliges Codesegment mehrmals verwendet wird, empfiehlt es sich, es nach Möglichkeit in eine Funktion zu verpacken.

def time_ago(delta, unit):
    delta_str = _number_to_text(delta) + " " + unit;
    if delta == 1:
        return delta_str + " ago"
    else:
        return delta_str = "s ago"

now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
    return "Today"

yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
    return "Yesterday"

delta = (now - event_date).days

if delta < 7:
    return time_ago(delta, "day")

if delta < 30:
    weeks = math.floor(delta / 7)
    return time_ago(weeks, "week")

if delta < 365:
    months = math.floor(delta / 31)
    return time_ago(months, "month")
Antonio Perez
quelle
5

Testgetriebene Entwicklung hat nicht geholfen.

TDD funktioniert am besten als Technik, wenn die Person, die die Tests schreibt, kontrovers ist. Dies ist schwierig, wenn Sie nicht paarweise programmieren.

  • Schreiben Sie keine Tests, um zu bestätigen, dass die zu testende Funktion so funktioniert, wie Sie sie erstellt haben. Schreiben Sie Tests, die absichtlich brechen.

Dies ist eine andere Technik, die sich auf das Schreiben von korrektem Code mit oder ohne TDD bezieht, und eine, die möglicherweise genauso komplex ist (wenn nicht sogar komplexer) als das eigentliche Schreiben von Code. Es ist etwas, was Sie üben müssen, und es gibt keine einfache Antwort darauf.

Die Kerntechnik zum Schreiben robuster Software ist auch die Kerntechnik zum Verstehen, wie effektive Tests geschrieben werden:

Verstehen Sie die Voraussetzungen für eine Funktion - die gültigen Zustände (dh welche Annahmen treffen Sie über den Zustand der Klasse, für die die Funktion eine Methode ist) und die gültigen Eingabeparameterbereiche - jeder Datentyp verfügt über einen Bereich möglicher Werte - von denen eine Teilmenge wird von Ihrer Funktion behandelt.

Wenn Sie lediglich diese Annahmen bei der Funktionseingabe explizit testen und sicherstellen, dass ein Verstoß protokolliert oder geworfen wird und / oder die Funktionsfehler ohne weitere Bearbeitung behoben werden, können Sie schnell feststellen, ob Ihre Software in der Produktion fehlerhaft ist und fehlertolerant, und entwickeln Sie Ihre konträren Testschreibfähigkeiten.


NB. Es gibt eine ganze Literatur zu Vor- und Nachbedingungen, Invarianten usw. sowie Bibliotheken, die sie mithilfe von Attributen anwenden können. Persönlich mag ich es nicht so förmlich zu gehen, aber es lohnt sich, mich damit zu befassen.

Chris Becke
quelle
1

Dies ist eine der wichtigsten Tatsachen bei der Softwareentwicklung: Es ist absolut unmöglich, fehlerfreien Code zu schreiben.

TDD wird Sie nicht davor bewahren, Fehler einzuführen, die Testfällen entsprechen, an die Sie nicht gedacht haben. Es wird Sie auch nicht davor bewahren, einen falschen Test zu schreiben, ohne es zu merken, und dann falschen Code zu schreiben, der den Buggy-Test besteht. Und jede andere Softwareentwicklungstechnik, die jemals erstellt wurde, weist ähnliche Lücken auf. Als Entwickler sind wir unvollkommene Menschen. Letztendlich gibt es keine Möglichkeit, 100% fehlerfreien Code zu schreiben. Es hat und wird nie passieren.

Das soll nicht heißen, dass du die Hoffnung aufgeben sollst. Während es unmöglich ist, vollkommen perfekten Code zu schreiben, ist es sehr gut möglich, Code zu schreiben, der so wenige Fehler aufweist, die in so seltenen Randfällen auftreten, dass die Software äußerst praktisch zu verwenden ist. Software, die in der Praxis kein fehlerhaftes Verhalten aufweist , kann sehr gut geschrieben werden.

Für das Schreiben müssen wir uns jedoch der Tatsache bewusst sein, dass wir fehlerhafte Software produzieren werden. Fast jede moderne Softwareentwicklungspraxis basiert auf dem Ziel, entweder das Auftreten von Fehlern zu verhindern oder uns vor den Folgen der Fehler zu schützen, die wir unvermeidlich verursachen:

  • Durch die Erfassung gründlicher Anforderungen können wir feststellen, wie falsch das Verhalten in unserem Code aussieht.
  • Das Schreiben von sauberem, sorgfältig zusammengestelltem Code erleichtert es, das erstmalige Auftreten von Fehlern zu vermeiden und diese leichter zu beheben, wenn wir sie identifizieren.
  • Das Schreiben von Tests ermöglicht es uns, eine Aufzeichnung dessen zu erstellen, von dem wir glauben, dass es sich bei vielen der schlimmsten Fehler in unserer Software handelt, und zu beweisen, dass wir zumindest diese Fehler vermeiden. TDD erstellt diese Tests vor dem Code, BDD leitet diese Tests aus den Anforderungen ab und altmodische Komponententests erstellen Tests, nachdem der Code geschrieben wurde, aber alle verhindern die schlimmsten Regressionen in der Zukunft.
  • Peer Reviews bedeuten, dass jedes Mal, wenn der Code geändert wird, mindestens zwei Augenpaare den Code gesehen haben, wodurch die Häufigkeit verringert wird, mit der Fehler in den Master eingedrungen sind.
  • Die Verwendung eines Bug-Trackers oder eines User-Story-Trackers, der Bugs als User-Storys behandelt, bedeutet, dass Bugs, die auftauchen, nachverfolgt und letztendlich behoben werden, nicht vergessen werden und den Benutzern beständig im Weg stehen.
  • Die Verwendung eines Staging-Servers bedeutet, dass vor einer Hauptversion alle Show-Stopper-Bugs auftauchen und behoben werden können.
  • Die Verwendung der Versionskontrolle bedeutet, dass Sie im schlimmsten Fall, in dem Code mit schwerwiegenden Fehlern an Kunden versendet wird, einen Notfall-Rollback durchführen und ein zuverlässiges Produkt wieder in die Hände Ihrer Kunden bringen können, während Sie die Dinge regeln.

Die ultimative Lösung für das von Ihnen identifizierte Problem besteht nicht darin, die Tatsache zu bekämpfen, dass Sie nicht garantieren können, dass Sie fehlerfreien Code schreiben, sondern es zu akzeptieren. Umfassen Sie branchenweit bewährte Methoden in allen Bereichen Ihres Entwicklungsprozesses, und stellen Sie Ihren Benutzern beständig Code zur Verfügung, der zwar nicht ganz perfekt ist, aber für den Job mehr als robust genug ist.

Kevin
quelle
1

An diesen Fall haben Sie bisher einfach nicht gedacht und hatten deshalb keinen Testfall dafür.

Dies passiert die ganze Zeit und ist normal. Es ist immer ein Kompromiss, wie viel Aufwand Sie in die Erstellung aller möglichen Testfälle gesteckt haben. Sie können unendlich viel Zeit aufwenden, um alle Testfälle zu betrachten.

Für einen Flugzeug-Autopiloten würden Sie viel mehr Zeit als für ein einfaches Werkzeug verbringen.

Es ist oft hilfreich, über die gültigen Bereiche Ihrer Eingabevariablen nachzudenken und diese Grenzen zu testen.

Wenn der Tester eine andere Person als der Entwickler ist, werden häufig schwerwiegendere Fälle gefunden.

Simon
quelle
1

(und glauben, dass es trotz der einheitlichen Verwendung von UTC im Code mit Zeitzonen zu tun hat)

Das ist ein weiterer logischer Fehler in Ihrem Code, für den Sie noch keinen Komponententest haben :) - Ihre Methode gibt falsche Ergebnisse für Benutzer in Nicht-UTC-Zeitzonen zurück. Sie müssen sowohl das "Jetzt" als auch das Datum des Ereignisses in die lokale Zeitzone des Benutzers konvertieren, bevor Sie die Berechnung durchführen können.

Beispiel: In Australien findet ein Ereignis um 9 Uhr Ortszeit statt. Um 11 Uhr wird es als "gestern" angezeigt, da sich das UTC-Datum geändert hat.

Sergey
quelle
0
  • Lassen Sie die Tests von jemand anderem schreiben. Auf diese Weise kann jemand, der mit Ihrer Implementierung nicht vertraut ist, nach seltenen Situationen suchen, an die Sie nicht gedacht haben.

  • Wenn möglich, injizieren Sie Testfälle als Sammlungen. Dies macht das Hinzufügen eines weiteren Tests so einfach wie das Hinzufügen einer weiteren Zeile yield return new TestCase(...). Dies kann in Richtung Erkundungstests gehen und die Erstellung von Testfällen automatisieren: "Mal sehen, was der Code für alle Sekunden einer Woche zurückgibt".

Null
quelle
0

Sie scheinen die falsche Vorstellung zu haben, dass Sie keine Fehler haben, wenn alle Ihre Tests bestanden werden. In Wirklichkeit ist das bekannte Verhalten korrekt , wenn alle Ihre Tests bestanden wurden . Sie wissen immer noch nicht, ob das unbekannte Verhalten korrekt ist oder nicht.

Hoffentlich verwenden Sie die Codeabdeckung mit Ihrem TDD. Fügen Sie einen neuen Test für das unerwartete Verhalten hinzu. Dann können Sie nur den Test für das unerwartete Verhalten ausführen, um zu sehen, welchen Pfad es tatsächlich durch den Code nimmt. Sobald Sie das aktuelle Verhalten kennen, können Sie eine Änderung vornehmen, um es zu korrigieren. Wenn alle Tests erneut bestanden werden, wissen Sie, dass Sie es ordnungsgemäß durchgeführt haben.

Dies bedeutet jedoch nicht, dass Ihr Code fehlerfrei ist, sondern dass er besser ist als zuvor, und das bekannte Verhalten stimmt erneut!

Die korrekte Verwendung von TDD bedeutet nicht, dass Sie fehlerfreien Code schreiben, sondern, dass Sie weniger Fehler schreiben. Du sagst:

Die Anforderungen waren relativ klar

Bedeutet dies, dass das Verhalten von mehr als einem Tag, aber nicht von gestern in den Anforderungen angegeben wurde? Wenn Sie eine schriftliche Anforderung verpasst haben, ist dies Ihre Schuld. Wenn Sie festgestellt haben, dass die Anforderungen beim Codieren unvollständig waren, ist das gut für Sie! Wenn alle, die an den Anforderungen gearbeitet haben, diesen Fall verpasst haben, sind Sie nicht schlechter als die anderen. Jeder macht Fehler und je subtiler sie sind, desto leichter können sie übersehen werden. Das große Problem dabei ist, dass TDD nicht alle Fehler verhindert !

CJ Dennis
quelle
0

Es ist sehr einfach, einen logischen Fehler zu begehen, selbst in einem solch einfachen Quellcode.

Ja. Testgetriebene Entwicklung ändert daran nichts. Sie können weiterhin Fehler im eigentlichen Code und auch im Testcode erstellen.

Testgetriebene Entwicklung hat nicht geholfen.

Oh, aber es ist passiert! Als Sie den Fehler bemerkten, hatten Sie bereits das komplette Test-Framework installiert und mussten lediglich den Fehler im Test (und den tatsächlichen Code) beheben. Zweitens wissen Sie nicht, wie viele Fehler Sie gehabt hätten, wenn Sie anfangs kein TDD durchgeführt hätten.

Ebenfalls besorgniserregend ist, dass ich nicht sehen kann, wie solche Fehler vermieden werden können.

Das kannst du nicht. Nicht einmal die NASA hat einen Weg gefunden, um Bugs zu vermeiden. wir kleineren Menschen sicherlich auch nicht.

Abgesehen davon, mehr darüber nachzudenken, bevor Sie Code schreiben,

Das ist ein Irrtum. Einer der größten Vorteile von TDD ist, dass Sie mit weniger Nachdenken programmieren können , da all diese Tests zumindest Regressionen ziemlich gut auffangen. Auch und gerade bei TDD ist nicht zu erwarten, dass fehlerfreier Code bereitgestellt wird (oder Ihre Entwicklungsgeschwindigkeit wird einfach zum Stillstand kommen).

Die einzige Möglichkeit, die mir in den Sinn kommt, besteht darin, für die Fälle, von denen ich glaube, dass sie niemals eintreten würden (so wie ich dachte, dass ein Tag zuvor notwendigerweise gestern ist), eine Reihe von Behauptungen hinzuzufügen und dann in den letzten zehn Jahren jede Sekunde zu durchlaufen und zu überprüfen, ob dies der Fall ist jede Behauptungsverletzung, die zu komplex erscheint.

Dies würde eindeutig dem Grundsatz widersprechen, nur das zu codieren, was Sie gerade tatsächlich benötigen. Sie dachten, Sie brauchen diese Fälle, und so war es auch. Es war ein unkritischer Code; Wie Sie sagten, gab es keinen Schaden, außer dass Sie sich 30 Minuten lang darüber wunderten.

Für unternehmenskritischen Code könnten Sie tatsächlich das tun, was Sie gesagt haben, aber nicht für Ihren alltäglichen Standardcode.

Wie könnte ich es vermeiden, diesen Bug überhaupt zu erzeugen?

Das tust du nicht. Sie vertrauen auf Ihre Tests, um die meisten Regressionen zu finden. Sie halten sich an den Rot-Grün-Refaktor-Zyklus, schreiben Tests vor / während der eigentlichen Codierung und (wichtig!) Sie implementieren den Mindestbetrag, der für den Rot-Grün-Wechsel erforderlich ist (nicht mehr, nicht weniger). Dies wird zu einer großartigen Testabdeckung führen, zumindest zu einer positiven.

Wenn Sie einen Fehler finden und nicht, schreiben Sie einen Test, um diesen Fehler zu reproduzieren, und beheben Sie den Fehler mit dem geringsten Arbeitsaufwand, damit dieser Test von rot auf grün wechselt.

AnoE
quelle
-2

Sie haben gerade festgestellt, dass Sie, egal wie sehr Sie sich bemühen, niemals alle möglichen Fehler in Ihrem Code finden können.

Das bedeutet also, dass selbst der Versuch, alle Fehler zu finden, eine vergebliche Übung ist. Daher sollten Sie nur Techniken wie TDD verwenden, um besseren Code zu schreiben, Code, der weniger Fehler enthält, nicht 0 Fehler.

Das wiederum bedeutet, dass Sie weniger Zeit mit diesen Techniken verbringen sollten und dass Sie Zeit gespart haben, um nach alternativen Wegen zu suchen, um die Fehler zu finden, die durch das Entwicklungsnetz gleiten.

Alternativen wie Integrationstests oder ein Testteam, Systemtests sowie Protokollierung und Analyse dieser Protokolle.

Wenn Sie nicht alle Bugs abfangen können, müssen Sie eine Strategie haben, um die Auswirkungen der Bugs, die an Ihnen vorbeischleichen, zu mindern. Wenn Sie dies trotzdem tun müssen, ist es sinnvoller, mehr Anstrengungen zu unternehmen, als (vergeblich) zu versuchen, sie überhaupt zu stoppen.

Schließlich ist es sinnlos, ein Vermögen in das Schreiben von Tests zu investieren, und am ersten Tag, an dem Sie einem Kunden Ihr Produkt geben, fällt es um, insbesondere wenn Sie dann keine Ahnung haben, wie Sie diesen Fehler finden und beheben können. Die Lösung von Post-Mortem- und Post-Delivery-Fehlern ist so wichtig und erfordert mehr Aufmerksamkeit, als die meisten Leute für das Schreiben von Unit-Tests ausgeben. Speichern Sie die Unit-Tests für die komplizierten Teile und versuchen Sie nicht, sie von vornherein zu perfektionieren.

gbjbaanb
quelle
Dies ist äußerst besiegt. That in turn means you should spend less time using these techniques- Aber du hast gerade gesagt, dass es mit weniger Fehlern helfen wird ?!
17.
@ JᴀʏMᴇᴇ mehr eine pragmatische Haltung von denen Technik , die Sie am meisten für Ihr buck.I Menschen kennen lernt , die stolz sind , dass sie 10 - mal Schreiben von Tests ausgeben , als sie auf ihren Code haben, und sie haben immer noch Bugs So sinnvoll zu sein, eher als dogmatisch, über Testtechniken ist von wesentlicher Bedeutung. Und Integrationstests müssen sowieso verwendet werden, also investieren Sie mehr in sie als in den Komponententest.
Gbjbaanb