Angenommen, Sie haben eine Funktion, die einen Unionstyp annimmt und dann den Typ einschränkt und an eine von zwei anderen reinen Funktionen delegiert.
function foo(arg: string|number) {
if (typeof arg === 'string') {
return fnForString(arg)
} else {
return fnForNumber(arg)
}
}
Nehmen wir an, dass fnForString()
und fnForNumber()
sind auch reine Funktionen, und sie wurden bereits selbst getestet.
Wie soll man testen foo()
?
- Sollten Sie die Tatsache, dass es an
fnForString()
undfnForNumber()
als Implementierungsdetail delegiert, behandeln und die Tests für jeden von ihnen im Wesentlichen duplizieren, wenn Sie die Tests für schreibenfoo()
? Ist diese Wiederholung akzeptabel? - Sollten Sie Tests schreiben, die diesen
foo()
Delegierten "kennen"fnForString()
undfnForNumber()
z. B. indem Sie sie verspotten und überprüfen, ob sie an sie delegieren?
javascript
typescript
testing
functional-programming
Samfrances
quelle
quelle
Antworten:
In einer idealen Welt würden Sie Beweise anstelle von Tests schreiben. Betrachten Sie beispielsweise die folgenden Funktionen.
Sagen Sie bitte, dass beweisen wollen
transform
angewendet zweimal ist idempotent , dh für alle gültigen Eingabenx
,transform(transform(x))
ist gleichx
. Nun, Sie müssten zuerst beweisen, dassnegate
undreverse
zweimal angewendet sind idempotent. Nehmen wir nun an, dass es trivial ist, die Idempotenz von zu beweisennegate
undreverse
zweimal anzuwenden, dh der Compiler kann es herausfinden. Somit haben wir die folgenden Deckspelzen .Wir können diese beiden Deckspelzen verwenden, um zu beweisen, dass dies
transform
wie folgt idempotent ist.Hier ist viel los, also lasst es uns zusammenfassen.
a|b
ein Vereinigungstyp unda&b
ein Schnittpunkttypa≡b
ein Gleichheitstyp ist.x
eines Gleichheitstypsa≡b
ist ein Beweis für die Gleichheit vona
undb
.a
undb
nicht gleich sind, ist es unmöglich, einen Wert vom Typ zu konstruierena≡b
.refl
, kurz für Reflexivität , hat den Typa≡a
. Es ist der triviale Beweis dafür, dass ein Wert sich selbst gleich ist.refl
im Beweis vonnegateNegateIdempotent
und verwendetreverseReverseIdempotent
. Dies ist möglich, weil die Sätze so trivial sind, dass der Compiler sie automatisch beweisen kann.negateNegateIdempotent
undreverseReverseIdempotent
Lemmas, um zu beweisentransformTransformIdempotent
. Dies ist ein Beispiel für einen nicht trivialen Beweis.Das Schreiben von Proofs hat den Vorteil, dass der Compiler den Proof überprüft. Wenn der Beweis falsch ist, kann das Programm check nicht eingeben und der Compiler gibt einen Fehler aus. Beweise sind aus zwei Gründen besser als Tests. Zunächst müssen Sie keine Testdaten erstellen. Es ist schwierig, Testdaten zu erstellen, die alle Randfälle behandeln. Zweitens werden Sie nicht versehentlich vergessen, Randfälle zu testen. Der Compiler gibt in diesem Fall einen Fehler aus.
Leider hat TypeScript keinen Gleichheitstyp, da es keine abhängigen Typen unterstützt, dh Typen, die von Werten abhängen. Daher können Sie keine Proofs in TypeScript schreiben. Sie können Proofs in abhängig typisierten funktionalen Programmiersprachen wie Agda schreiben .
Sie können jedoch Vorschläge in TypeScript schreiben.
Sie können dann eine Bibliothek wie jsverify verwenden, um automatisch Testdaten für mehrere Testfälle zu generieren.
Sie können auch anrufen
jsc.forall
mit ,"number | string"
aber ich kann nicht scheinen , um es an die Arbeit zu machen.Also, um deine Fragen zu beantworten.
Die funktionale Programmierung fördert das eigenschaftsbasierte Testen. Zum Beispiel habe ich getestet , die
negate
,reverse
undtransform
Funktionen zweimal für idempotence angewandt. Wenn Sie eigenschaftsbasierten Tests folgen, sollten Ihre Satzfunktionen in ihrer Struktur den Funktionen ähneln, die Sie testen.Ja, ist das akzeptabel? Sie können jedoch ganz auf das Testen verzichten
fnForString
undfnForNumber
weil die Tests für diese in den Tests für enthalten sindfoo
. Der Vollständigkeit halber würde ich jedoch empfehlen, alle Tests einzuschließen, auch wenn dadurch Redundanz eingeführt wird.Die Aussagen, die Sie beim eigenschaftsbasierten Testen schreiben, folgen der Struktur der Funktionen, die Sie testen. Daher "kennen" sie die Abhängigkeiten, indem sie die Sätze der anderen getesteten Funktionen verwenden. Keine Notwendigkeit, sie zu verspotten. Sie müssen nur Dinge wie Netzwerkanrufe, Dateisystemaufrufe usw. verspotten.
quelle
Die beste Lösung wäre nur zu testen
foo
.fnForString
undfnForNumber
sind ein Implementierungsdetail, das Sie möglicherweise in Zukunft ändern werden, ohne das Verhalten von unbedingt zu ändernfoo
. Wenn dies passiert, können Ihre Tests ohne Grund unterbrochen werden. Diese Art von Problem macht Ihren Test zu umfangreich und nutzlos.Ihre Schnittstelle braucht
foo
nur, testen Sie es einfach.Wenn Sie diese Art von Test testen
fnForString
undfnForNumber
von Ihren öffentlichen Schnittstellentests fernhalten müssen.Dies ist meine Interpretation des folgenden von Kent Beck angegebenen Prinzips
quelle
Kurze Antwort: Die Spezifikation einer Funktion bestimmt, wie sie getestet werden soll.
Lange Antwort:
Testen = Verwenden einer Reihe von Testfällen (hoffentlich repräsentativ für alle möglicherweise auftretenden Fälle), um zu überprüfen, ob eine Implementierung ihre Spezifikation erfüllt.
In dem Beispiel wird foo ohne Angabe angegeben, daher sollte man foo testen, indem man überhaupt nichts tut (oder höchstens einige alberne Tests, um die implizite Anforderung zu überprüfen, dass "foo auf die eine oder andere Weise endet").
Wenn die Spezifikation betriebsbereit ist wie "Diese Funktion gibt das Ergebnis der Anwendung von Argumenten auf fnForString oder fnForNumber je nach Art der Argumente zurück", ist das Verspotten der Delegaten (Option 2) der richtige Weg. Unabhängig davon, was mit fnForString / Number passiert, bleibt foo in Übereinstimmung mit seiner Spezifikation.
Wenn die Spezifikation nicht auf diese Weise von fnForType abhängt, ist die Wiederverwendung der Tests für fnFortype (Option 1) der richtige Weg (vorausgesetzt, diese Tests sind gut).
Beachten Sie, dass die Betriebsspezifikationen einen Großteil der üblichen Freiheit, eine Implementierung durch eine andere zu ersetzen (eine, die eleganter / lesbarer / effizienter / usw. ist), aufhebt. Sie sollten nur nach sorgfältiger Überlegung verwendet werden.
quelle
Nun , da Implementierungsdetails werden delegieren
fnForString()
undfnForNumber()
fürstring
undnumber
jeweils zu testen es läuft darauf hinaus, nur nach unten stellen Sie sicher , dassfoo
Anrufe die richtige Funktion. Also ja, ich würde sie verspotten und sicherstellen, dass sie entsprechend aufgerufen werden.Da
fnForString()
undfnForNumber()
einzeln getestet wurden, wissen Sie, dass beimfoo()
Aufrufen die richtige Funktion aufgerufen wird und Sie wissen, dass die Funktion das tut, was sie tun soll.foo sollte etwas zurückgeben. Sie könnten etwas von Ihren Mocks zurückgeben, jedes etwas anderes und sicherstellen, dass foo korrekt zurückkehrt (zum Beispiel, wenn Sie eine
return
in Ihrer foo- Funktion vergessen haben ).Und alle Dinge wurden abgedeckt.
quelle
Ich denke, es ist sinnlos, den Typ Ihrer Funktion zu testen. Das System kann dies alleine tun und es Ihnen ermöglichen, jedem der Objekttypen, die Sie interessieren, den gleichen Namen zu geben
Beispielcode
Ich bin wahrscheinlich kein großer Theoretiker für Codierungstechniken, aber ich habe viel Code gepflegt. Ich denke, dass man die Wirksamkeit einer Kodierungsmethode nur in konkreten Fällen wirklich beurteilen kann, die Spekulation kann keinen Beweiswert haben.
quelle