In einer idealen Welt würden Sie Beweise anstelle von Tests schreiben. Betrachten Sie beispielsweise die folgenden Funktionen.
const negate = (x: number): number => -x;
const reverse = (x: string): string => x.split("").reverse().join("");
const transform = (x: number|string): number|string => {
switch (typeof x) {
case "number": return negate(x);
case "string": return reverse(x);
}
};
Sagen Sie bitte, dass beweisen wollen transform
angewendet zweimal ist idempotent , dh für alle gültigen Eingaben x
, transform(transform(x))
ist gleich x
. Nun, Sie müssten zuerst beweisen, dass negate
und reverse
zweimal angewendet sind idempotent. Nehmen wir nun an, dass es trivial ist, die Idempotenz von zu beweisen negate
und reverse
zweimal anzuwenden, dh der Compiler kann es herausfinden. Somit haben wir die folgenden Deckspelzen .
const negateNegateIdempotent = (x: number): negate(negate(x))≡x => refl;
const reverseReverseIdempotent = (x: string): reverse(reverse(x))≡x => refl;
Wir können diese beiden Deckspelzen verwenden, um zu beweisen, dass dies transform
wie folgt idempotent ist.
const transformTransformIdempotent = (x: number|string): transform(transform(x))≡x => {
switch (typeof x) {
case "number": return negateNegateIdempotent(x);
case "string": return reverseReverseIdempotent(x);
}
};
Hier ist viel los, also lasst es uns zusammenfassen.
- Ebenso wie
a|b
ein Vereinigungstyp und a&b
ein Schnittpunkttyp a≡b
ein Gleichheitstyp ist.
- Ein Wert
x
eines Gleichheitstyps a≡b
ist ein Beweis für die Gleichheit von a
und b
.
- Wenn zwei Werte
a
und b
nicht gleich sind, ist es unmöglich, einen Wert vom Typ zu konstruieren a≡b
.
- Der Wert
refl
, kurz für Reflexivität , hat den Typ a≡a
. Es ist der triviale Beweis dafür, dass ein Wert sich selbst gleich ist.
- Wir haben
refl
im Beweis von negateNegateIdempotent
und verwendet reverseReverseIdempotent
. Dies ist möglich, weil die Sätze so trivial sind, dass der Compiler sie automatisch beweisen kann.
- Wir verwenden die
negateNegateIdempotent
und reverseReverseIdempotent
Lemmas, um zu beweisen transformTransformIdempotent
. 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.
const negateNegateIdempotent = (x: number): boolean => negate(negate(x)) === x;
const reverseReverseIdempotent = (x: string): boolean => reverse(reverse(x)) === x;
const transformTransformIdempotent = (x: number|string): boolean => {
switch (typeof x) {
case "number": return negateNegateIdempotent(x);
case "string": return reverseReverseIdempotent(x);
}
};
Sie können dann eine Bibliothek wie jsverify verwenden, um automatisch Testdaten für mehrere Testfälle zu generieren.
const jsc = require("jsverify");
jsc.assert(jsc.forall("number", transformTransformIdempotent)); // OK, passed 100 tests
jsc.assert(jsc.forall("string", transformTransformIdempotent)); // OK, passed 100 tests
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.
Wie soll man testen foo()
?
Die funktionale Programmierung fördert das eigenschaftsbasierte Testen. Zum Beispiel habe ich getestet , die negate
, reverse
und transform
Funktionen zweimal für idempotence angewandt. Wenn Sie eigenschaftsbasierten Tests folgen, sollten Ihre Satzfunktionen in ihrer Struktur den Funktionen ähneln, die Sie testen.
Sollten Sie die Tatsache, dass es an fnForString()
und fnForNumber()
als Implementierungsdetail delegiert, behandeln und die Tests für jeden von ihnen im Wesentlichen duplizieren, wenn Sie die Tests für schreiben foo()
? Ist diese Wiederholung akzeptabel?
Ja, ist das akzeptabel? Sie können jedoch ganz auf das Testen verzichten fnForString
und fnForNumber
weil die Tests für diese in den Tests für enthalten sind foo
. Der Vollständigkeit halber würde ich jedoch empfehlen, alle Tests einzuschließen, auch wenn dadurch Redundanz eingeführt wird.
Sollten Sie Tests schreiben, die diesen foo()
Delegierten "kennen" fnForString()
und fnForNumber()
z. B. indem Sie sie verspotten und überprüfen, ob sie an sie delegieren?
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.