Wie würden Tippfehler beim Erstellen von Mocks in einer dynamischen Sprache erkannt?

10

Das Problem tritt beim TDD auf. Nach einigen Testdurchläufen ändern sich die Rückgabetypen einiger Klassen / Module. Wenn in einer statisch typisierten Programmiersprache ein zuvor verspottetes Objekt in den Tests einer anderen Klasse verwendet wurde und nicht geändert wurde, um die Typänderung widerzuspiegeln, treten Kompilierungsfehler auf.

Bei dynamischen Sprachen wird die Änderung der Rückgabetypen möglicherweise nicht erkannt, und die Tests der anderen Klasse werden weiterhin bestanden. Sicher, es könnte Integrationstests geben, die später fehlschlagen sollten, aber Unit-Tests würden fälschlicherweise bestanden. Gibt es eine Möglichkeit, dies zu vermeiden?

Aktualisierung mit einem trivialen Beispiel (in einer erfundenen Sprache) ...

Version 1:

Calc = {
    doMultiply(x, y) {return x * y}
}
//.... more code ....

// On some faraway remote code on a different file
Rect = {
    computeArea(l, w) {return Calc.doMultipy(x*y)}
}

// test for Rect
testComputeArea() { 
    Calc = new Mock()
    Calc.expect(doMultiply, 2, 30) // where 2 is the arity
    assertEqual(30, computeArea)
}

Nun zu Version 2:

// I change the return types. I also update the tests for Calc
Calc = {
    doMultiply(x, y) {return {result: (x * y), success:true}}
}

... Rect löst dann zur Laufzeit eine Ausnahme aus, der Test ist jedoch weiterhin erfolgreich.

jvliwanag
quelle
1
Was die Antworten bisher zu vermissen scheinen, ist, dass es bei der Frage nicht um die Tests geht, die das Geänderte betreffen class X, sondern deren Tests class Ydavon abhängen Xund somit gegen einen anderen Vertrag getestet werden als gegen das, gegen das es in der Produktion läuft.
Bart van Ingen Schenau
Ich habe diese Frage gerade bei SO selbst in Bezug auf die Abhängigkeitsinjektion gestellt. Siehe Grund 1: Eine abhängige Klasse kann zur Laufzeit geändert werden (Think Testing) . Wir haben beide die gleiche Einstellung, aber es fehlen großartige Erklärungen.
Scott Coates
Ich lese Ihre Frage noch einmal, bin aber etwas verwirrt über die Interpretation. Können Sie ein Beispiel geben?
Scott Coates

Antworten:

2

Bis zu einem gewissen Grad ist dies nur ein Teil der Kosten für das Geschäft mit dynamischen Sprachen. Sie erhalten viel Flexibilität, auch bekannt als "genug Seil, um sich aufzuhängen". Sei vorsichtig damit.

Für mich schlägt das Problem vor, andere Refactoring-Techniken zu verwenden als in einer statisch typisierten Sprache. In einer statischen Sprache ersetzen Sie Rückgabetypen teilweise, damit Sie sich auf den Compiler stützen können, um herauszufinden, welche Stellen möglicherweise beschädigt werden. Dies ist sicher und wahrscheinlich sicherer als das Ändern des vorhandenen Rückgabetyps.

In einer dynamischen Sprache ist dies nicht möglich. Daher ist es viel sicherer, den Rückgabetyp zu ändern, als ihn zu ersetzen. Möglicherweise ändern Sie es, indem Sie Ihre neue Klasse als Mitglied für die Klassen hinzufügen, die es benötigen.

Tallseth
quelle
2

Wenn sich Ihr Code ändert und Ihre Tests immer noch bestanden werden, stimmt entweder etwas mit Ihren Tests nicht (dh Sie vermissen eine Behauptung), oder der Code hat sich nicht wirklich geändert.

Was meine ich damit? Nun, Ihre Tests beschreiben die Verträge zwischen Teilen Ihres Codes. Wenn der Vertrag "der Rückgabewert muss iterierbar sein" lautet, ist das Ändern des Rückgabewerts von beispielsweise einem Array in eine Liste keine Vertragsänderung und löst daher nicht unbedingt einen Testfehler aus.

Um fehlende Zusicherungen zu vermeiden, können Sie Tools wie die Analyse der Codeabdeckung (sie sagt Ihnen nicht, welche Teile Ihres Codes getestet werden, aber es wird Ihnen sagen, welche Teile definitiv nicht getestet werden), die zyklomatische Komplexität und die NPath-Komplexität verwenden (welche geben Sie eine untere Schranke für die Anzahl der Behauptungen voll C1 und C2 - Code - Abdeckung zu erreichen erforderlich) und Mutation Tester (die inject Mutationen in Ihrem Code wie Drehen trueauf false, negative Zahlen in positive, in Objekte nullusw. und prüfen , ob die macht Tests fehlgeschlagen).

Jörg W Mittag
quelle
+1 Entweder hat sich etwas mit Tests nicht geändert, oder der Code hat sich nicht geändert. Ich habe eine Weile gebraucht, um mich nach Jahren von C ++ / Java daran zu gewöhnen. Der Vertrag zwischen Teilen in dynamischen Codesprachen sollte nicht lauten, WAS zurückgegeben wird, sondern was das zurückgegebene Objekt enthält und was es tun kann.
MrFox
@suslik: Eigentlich geht es hier nicht um statische oder dynamische Sprachen, sondern um Datenabstraktion mit abstrakten Datentypen oder objektorientierte Datenabstraktion. Die Fähigkeit eines Objekts, ein anderes Objekt zu simulieren, solange es sich nicht unterscheidbar verhält (selbst wenn es sich um völlig unterschiedliche Typen und Instanzen völlig unterschiedlicher Klassen handelt), ist für die gesamte Idee von OO von grundlegender Bedeutung . Oder anders ausgedrückt: Wenn sich zwei Objekte gleich verhalten, sind sie vom gleichen Typ, unabhängig davon, was ihre Klassen sagen. Anders ausgedrückt: Klassen sind keine Typen. Leider verstehen Java und C # das falsch.
Jörg W Mittag
Angenommen, die verspottete Einheit ist zu 100% abgedeckt und es fehlt keine Behauptung: Wie wäre es mit einer subtileren Änderung wie "sollte null für foo zurückgeben" zu "sollte leere Zeichenfolge für foo zurückgeben". Wenn jemand diese Einheit und ihren Test ändert, können einige verbrauchende Einheiten kaputt gehen und aufgrund des Scheines ist dies transparent. (Und nein: Zum Zeitpunkt des Schreibens oder der Verwendung des verspotteten Moduls ist es gemäß "Vertrag" nicht erforderlich, leere Zeichenfolgen als Rückgabewert zu behandeln, da nicht leere Zeichenfolgen oder nur Null zurückgegeben werden sollen.)) (Nur ein ordnungsgemäßer Integrationstest beider Module in Interaktion konnte dies
try-catch-finally