White-Box-Test

Die Entwicklung hochwertiger Programme setzt voraus, dass das Programm und seine Teile getestet werden. Beim klassischen Testen von Einheiten wird ein großes Programm in kleine Blöcke aufgeteilt, die zum Testen geeignet sind. Oder wenn die Entwicklung von Tests parallel zur Entwicklung von Code erfolgt oder Tests vor dem Programm entwickelt werden (TDD - Test Driven Development), wird das Programm zunächst in kleinen Blöcken entwickelt, die für die Anforderungen der Tests geeignet sind.


Eine der Varianten von Unit-Tests kann als eigenschaftsbasiertes Testen betrachtet werden (dieser Ansatz ist beispielsweise in den Bibliotheken QuickCheck , ScalaCheck implementiert ). Dieser Ansatz basiert auf der Suche nach universellen Eigenschaften, die für alle Eingabedaten gültig sein sollten. Beispielsweise sollte eine Serialisierung gefolgt von einer Deserialisierung dasselbe Objekt erzeugen . Durch erneutes Sortieren sollte die Reihenfolge der Elemente in der Liste nicht geändert werden . Um solche universellen Eigenschaften zu überprüfen, unterstützen die obigen Bibliotheken einen Mechanismus zum Generieren von zufälligen Eingabedaten. Dieser Ansatz eignet sich besonders gut für Programme, die auf mathematischen Gesetzen basieren und als universelle Eigenschaften für eine breite Klasse von Programmen dienen. Es gibt sogar eine Bibliothek mit vorgefertigten mathematischen Eigenschaften - Disziplin -, mit der Sie die Leistung dieser Eigenschaften in neuen Programmen überprüfen können (ein gutes Beispiel für die Wiederverwendung von Tests).


Manchmal stellt sich heraus, dass es notwendig ist, ein komplexes Programm zu testen, ohne es in unabhängig überprüfbare Teile zerlegen zu können. In diesem Fall ist das Testprogramm schwarz White Box (weiß - weil wir die Möglichkeit haben, die interne Struktur des Programms zu studieren).


Im Rahmen des Schnitts werden verschiedene Ansätze zum Testen komplexer Programme mit einer Eingabe mit unterschiedlichem Komplexitätsgrad (Beteiligung) und unterschiedlichem Abdeckungsgrad beschrieben.


* In diesem Artikel gehen wir davon aus, dass das zu testende Programm als reine Funktion ohne internen Zustand dargestellt werden kann. (Einige der folgenden Überlegungen können angewendet werden, wenn der interne Zustand vorliegt, es ist jedoch möglich, diesen Zustand auf einen festen Wert zurückzusetzen.)


Prüfstand


Da nur eine Funktion getestet wird, deren aufrufender Code immer der gleiche ist, müssen wir zunächst keine separaten Komponententests erstellen. Alle diese Tests wären gleich, genau auf die Eingabe und die Prüfungen. Es reicht völlig aus, die Quelldaten ( input ) in einer Schleife zu übertragen und die Ergebnisse ( expectedOutput ) zu überprüfen. Um einen Problemsatz von Testdaten im Falle einer Fehlererkennung zu identifizieren, müssen alle Testdaten gekennzeichnet werden. Somit kann ein Satz von Testdaten als Dreifach dargestellt werden:


 case class TestCase[A, B](label: String, input: A, expectedOutput: B) 

Das Ergebnis eines Laufs kann als TestCaseResult :


 case class TestCaseResult[A, B](testCase: TestCase[A, B], actualOutput: Try[B]) 

(Wir präsentieren das Ergebnis des Starts mit Try , mögliche Ausnahmen abzufangen.)


Um die Ausführung aller Testdaten durch das zu testende Programm zu vereinfachen, können Sie eine Hilfsfunktion verwenden, die das Programm für jeden Eingabewert aufruft:


 def runTestCases[A, B](cases: Seq[TestCase[A, B])(f: A => B): Seq[TestCaseResult[A, B]] = cases .map{ testCase => TestCaseResult(testCase, Try{ f(testCase.input) } ) } .filter(r => r.actualOutput != Success(r.testCase.expectedOutput)) 

Diese Hilfsfunktion gibt die problematischen Daten und Ergebnisse zurück, die sich von den erwarteten unterscheiden.


Der Einfachheit halber können Sie die Testergebnisse formatieren.


 def report(results: Seq[TestCaseResult[_, _]]): String = s"Failed ${results.length}:\n" + results .map(r => r.testCase.label + ": expected " + r.testCase.expectedOutput + ", but got " + r.actualOutput) .mkString("\n") 

und zeigen Sie einen Bericht nur bei Fehlern an:


 val testCases = Seq( TestCase("1", 0, 0) ) test("all test cases"){ val testBench = runTestCases(testCases) _ val results = testBench(f) assert(results.isEmpty, report(results)) } 

Eingabevorbereitung


Im einfachsten Fall können Sie manuell Testdaten erstellen, um das Programm zu testen, es direkt in den Testcode zu schreiben und wie oben gezeigt zu verwenden. Es stellt sich oft heraus, dass interessante Fälle von Testdaten viel gemeinsam haben und mit geringfügigen Änderungen als grundlegende Instanz dargestellt werden können.


 val baseline = MyObject(...) //        val testCases = Seq( TestCase("baseline", baseline, ???), TestCase("baseline + (field1 = 123)", baseline.copy(field1 = "123"), ???) ) 

Bei der Arbeit mit verschachtelten unveränderlichen Datenstrukturen sind Objektive eine große Hilfe, beispielsweise aus der Monocle- Bibliothek:


 val baseline = ??? val testObject1 = (field1 composeLens field2).set("123")(baseline) //    : val testObject1 = baseline.copy(field1 = baseline.field1.copy(field2 = "123")) 

Mit Objektiven können Sie tief verschachtelte Teile von Datenstrukturen elegant „modifizieren“: Jedes Objektiv ist ein Getter und Setter für eine Eigenschaft. Objektive können kombiniert werden, um Objektive herzustellen, die sich auf die nächste Ebene "konzentrieren".


Verwenden von DSL zum Präsentieren von Änderungen


Als nächstes betrachten wir die Bildung von Testdaten, indem wir Änderungen an einem anfänglichen Eingabeobjekt vornehmen. Normalerweise müssen wir einige Änderungen vornehmen, um das benötigte Testobjekt zu erhalten. Gleichzeitig ist es sehr nützlich, eine Liste der Änderungen in die Textbeschreibung von TestCase aufzunehmen:


 val testCases = Seq( TestCase("baseline", baseline, ???), TestCase("baseline + " + "(field1 = 123) + " + //  1-  "(field2 = 456) + " + // 2- "(field3 = 789)", // 3- baseline .copy(field1 = "123") // 1-  .copy(field2 = "456") // 2-  .copy(field3 = "789"), // 3-  ???) ) 

Dann wissen wir immer, für welche Testdaten der Test durchgeführt wird.


Damit die Textliste der Änderungen nicht von den tatsächlichen Änderungen abweicht, müssen Sie dem Prinzip "einer einzigen Version der Wahrheit" folgen. (Wenn dieselbe Information an mehreren Stellen benötigt / verwendet wird, sollte es eine einzige primäre Quelle für eindeutige Informationen geben, und Informationen sollten automatisch mit den erforderlichen Transformationen an alle anderen Verwendungsstellen verteilt werden. Wenn dieses Prinzip verletzt wird und ein manuelles Kopieren von Informationen unvermeidlich ist . Diskrepanz Versionsinformationen an verschiedenen Punkten in anderen Worten in der Beschreibung der Testdaten, können wir ein und Testdaten sehen -. ein weiteres Beispiel, das Kopieren einer Änderung field2 = "456" und in der Einstellung field3 = "789" wir Mauger Vergessen versehentlich die Beschreibung zu korrigieren. Als Ergebnis wird die Beschreibung nur zwei Änderungen von drei reflektieren.)


In unserem Fall sind die Änderungen selbst die Hauptinformationsquelle bzw. der Quellcode des Programms, das die Änderungen vornimmt. Wir möchten daraus einen Text ableiten, der die Änderungen beschreibt. Nebenbei können Sie als erste Option die Verwendung eines Makros vorschlagen, das den Quellcode der Änderungen erfasst, und den Quellcode als Dokumentation verwenden. Dies ist anscheinend eine gute und relativ unkomplizierte Methode, um tatsächliche Änderungen zu dokumentieren, und kann in einigen Fällen durchaus angewendet werden. Wenn wir die Änderungen im Klartext darstellen, verlieren wir leider die Fähigkeit, sinnvolle Transformationen der Änderungsliste vorzunehmen. Erkennen und beseitigen Sie beispielsweise doppelte oder überlappende Änderungen und erstellen Sie eine Liste der Änderungen auf eine für den Endbenutzer bequeme Weise.


Um Änderungen verarbeiten zu können, müssen Sie über ein strukturiertes Modell verfügen. Das Modell sollte aussagekräftig genug sein, um alle Änderungen zu beschreiben, die uns interessieren. Teil dieses Modells ist beispielsweise die Adressierung von Objektfeldern, Konstanten und Zuweisungsoperationen.


Das Änderungsmodell sollte die Lösung der folgenden Aufgaben ermöglichen:


  1. Spawn-Änderungsmodellinstanzen. (Das heißt, tatsächlich wird eine bestimmte Liste von Änderungen erstellt.)
  2. Bildung einer Textbeschreibung der Änderungen.
  3. Anwenden von Änderungen auf Domänenobjekte.
  4. Durchführen von Optimierungstransformationen für das Modell.

Wenn eine universelle Programmiersprache verwendet wird, um Änderungen vorzunehmen, kann es schwierig sein, diese Änderungen im Modell darzustellen. Der Quellcode des Programms kann komplexe Konstruktionen verwenden, die vom Modell nicht unterstützt werden. Ein solches Programm kann sekundäre Muster wie Linsen oder die copy verwenden, um die Felder eines Objekts zu ändern, bei denen es sich um Abstraktionen niedrigerer Ebene relativ zur Ebene des Änderungsmodells handelt. Infolgedessen kann eine zusätzliche Analyse solcher Muster erforderlich sein, um Instanzen von Änderungen auszugeben. Daher ist eine gute Option mit einem Makro zunächst nicht sehr praktisch.


Eine andere Möglichkeit, Instanzen des Änderungsmodells zu erstellen, kann eine spezielle Sprache (DSL) sein, die Änderungsmodellobjekte mithilfe einer Reihe von Erweiterungsmethoden und Hilfsoperatoren erstellt. In den einfachsten Fällen können Instanzen des Änderungsmodells direkt über die Konstruktoren erstellt werden.


Sprachdetails ändern

Die Änderungssprache ist eine ziemlich komplexe Konstruktion, die mehrere Komponenten enthält, die wiederum nicht trivial sind.


  1. Datenstrukturmodell.
  2. Modell ändern.
  3. Tatsächlich eingebettetes (?) DSL - Hilfskonstruktionen, Erweiterungsmethoden zur bequemen Konstruktion von Änderungen.
  4. Ein Interpret von Änderungen, mit dem Sie ein Objekt tatsächlich "ändern" können (tatsächlich können Sie natürlich eine geänderte Kopie erstellen).

Hier ist ein Beispiel für ein Programm, das mit DSL geschrieben wurde:


 val target: Entity[Target] // ,     val updateField1 = target \ field1 := "123" val updateField2 = target \ subobject \ field2 := "456" // ,   DSL: val updateField1 = SetProperty(PropertyAccess(target, Property(field1, typeTag[String])), LiftedString("123")) val updateField2 = SetProperty(PropertyAccess(PropertyAccess(target, Property(subobject, typeTag[SubObject])), Property(field2, typeTag[String])), LiftedString("456")) 

Mit den Erweiterungsmethoden \ und := , PropertyAccess werden SetProperty Objekte aus den zuvor erstellten target , field1 , subobject und field2 . Aufgrund (gefährlicher) impliziter Konvertierungen wird die Zeichenfolge "123" in einen LiftedString gepackt (Sie können auf implizite Konvertierungen verzichten und die entsprechende Methode explizit aufrufen: lift("123") ).


Eine typisierte Ontologie kann als Datenmodell verwendet werden (siehe https://habr.com/post/229035/ und https://habr.com/post/222553/ ). (Kurz val field1: Property[Target, String] werden deklariert, die die Eigenschaften eines beliebigen val field1: Property[Target, String] : val field1: Property[Target, String] .) In diesem Fall können die tatsächlichen Daten beispielsweise in Form von JSON gespeichert werden. Die Bequemlichkeit einer typisierten Ontologie liegt in unserem Fall in der Tatsache, dass das Änderungsmodell normalerweise mit einzelnen Eigenschaften von Objekten arbeitet und die Ontologie lediglich ein geeignetes Werkzeug zum Adressieren von Eigenschaften bietet.


Um die Änderungen darzustellen, benötigen Sie eine Reihe von Klassen mit demselben Plan wie die SetProperty genannte SetProperty Klasse:


  • Modify - Anwendung der Funktion,
  • Changes - mehrere Änderungen nacheinander anwenden
  • ForEach - Änderungen auf jedes Element in der Sammlung anwenden,
  • usw.

Der Interpreter zum Ändern der Sprache ist ein regulärer Evaluator für rekursive Ausdrücke, der auf PatternMatching basiert. So etwas wie:


 def eval(expression: DslExpression, gamma: Map[String, Any]): Any = expression match { case LiftedString(str) => str case PropertyAccess(obj, prop) => Getter(prop)(gamma).get(obj) } def change[T] (expression: DslChangeExpression, gamma: Map[String, Any], target: T): T = expression match { case SetProperty(path, valueExpr) => val value = eval(valueExpr, gamma) Setter(path)(gamma).set(value)(target) } 

Um die Eigenschaften von Objekten direkt bearbeiten zu können, müssen Sie für jede im Änderungsmodell verwendete Eigenschaft Getter und Setter angeben. Dies kann erreicht werden, indem die Karte zwischen den ontologischen Eigenschaften und den entsprechenden Linsen ausgefüllt wird.


Dieser Ansatz als Ganzes funktioniert und ermöglicht es Ihnen, die Änderungen einmal zu beschreiben, aber nach und nach müssen immer komplexere Änderungen dargestellt werden, und das Modell der Änderungen wächst etwas. Wenn Sie beispielsweise eine Eigenschaft mithilfe des Werts einer anderen Eigenschaft desselben Objekts ändern müssen (z. B. field1 = field2 + 1 ), müssen Sie Variablen auf DSL-Ebene unterstützen. Wenn das Ändern einer Eigenschaft nicht trivial ist, ist auf DSL-Ebene die Unterstützung von arithmetischen Ausdrücken und Funktionen erforderlich.


Branchentests


Der Testcode kann linear sein, und dann reicht im Großen und Ganzen ein Satz von Testdaten aus, um zu verstehen, ob er funktioniert. Wenn es einen Zweig gibt ( if-then-else ), müssen Sie das weiße Feld mindestens zweimal mit unterschiedlichen Eingabedaten ausführen, damit beide Zweige ausgeführt werden. Die Anzahl der Sätze von Eingabedaten, die ausreichen, um alle Zweige abzudecken, entspricht anscheinend numerisch der zyklomatischen Komplexität des Codes mit Zweigen.


Wie bilde ich alle Sätze von Eingabedaten? Da es sich um eine weiße Box handelt, können wir die Verzweigungsbedingungen isolieren und das Eingabeobjekt zweimal ändern, sodass in einem Fall eine Verzweigung ausgeführt wird, in dem anderen Fall eine andere. Betrachten Sie ein Beispiel:


 if (object.field1 == "123") A else B 

Unter solchen Bedingungen können wir zwei Testfälle bilden:


 val testCase1 = TestCase("A", field1.set("123")(baseline), /* result of A */) val testCase2 = TestCase("B", field1.set(/*  "123", , , */"123" + "1">)(baseline), /*result of B*/) 

(Falls eines der Testszenarien nicht erstellt werden kann, können wir davon ausgehen, dass toter Code erkannt wurde und die Bedingung zusammen mit dem entsprechenden Zweig sicher entfernt werden kann.)


Wenn unabhängige Eigenschaften eines Objekts in mehreren Zweigen überprüft werden, ist es recht einfach, einen vollständigen Satz modifizierter Testobjekte zu bilden, der alle möglichen Kombinationen vollständig abdeckt.


DSL, um alle Kombinationen von Änderungen zu bilden

Lassen Sie uns den Mechanismus genauer betrachten, mit dem alle möglichen Änderungslisten erstellt werden können, die eine vollständige Abdeckung aller Zweige ermöglichen. Um die Liste der Änderungen während des Testens verwenden zu können, müssen wir alle Änderungen in einem Objekt zusammenfassen, das wir an die Eingabe des getesteten Codes senden, dh Unterstützung für die Komposition ist erforderlich. Dazu können Sie entweder das obige DSL verwenden, um Änderungen zu modellieren, und dann reicht eine einfache Liste von Änderungen aus, oder Sie können eine Änderung als Änderungsfunktion T => T :


 val change1: T => T = field1.set("123")(_) // val change1: T => T = _.copy(field1 = "123") val change2: T => T = field2.set("456") 

dann wird die Kette von Änderungen einfach eine Zusammensetzung von Funktionen sein:


 val changes = change1 compose change2 

oder für eine Liste von Änderungen:


 val rawChangesList: Seq[T => T] = Seq(change1, change2) val allChanges: T => T = rawChangesList.foldLeft(identity)(_ compose _) 

Um alle Änderungen, die allen möglichen Zweigen entsprechen, kompakt aufzuzeichnen, können Sie das DSL der folgenden Abstraktionsebene verwenden, das die Struktur der getesteten White Box simuliert:


 val tests: Seq[(String, T => T)] = IF("field1 == '123'") //  ,    THEN( field1.set("123"))( //  target \ field1 := "123" IF("field2 == '456') THEN(field2.set("456"))(TERMINATE) ELSE(field2.set("456" + "1"))(TERMINATE) ) ELSE( field1.set("123" + "1") )(TERMINATE) 

Hier enthält die Testsammlung aggregierte Änderungen, die allen möglichen Kombinationen von Zweigen entsprechen. Ein Parameter vom Typ String enthält alle Namen der Bedingungen und alle Beschreibungen der Änderungen, aus denen die aggregierte Änderungsfunktion gebildet wird. Und das zweite Element eines Paares vom Typ T => T ist nur die aggregierte Funktion von Änderungen, die sich aus der Zusammensetzung einzelner Änderungen ergeben.


Um die geänderten Objekte abzurufen, müssen Sie alle aggregierten Änderungsfunktionen auf das Basisobjekt anwenden:


 val tests2: Seq[(String, T)] = tests.map(_.map_2(_(baseline))) 

Als Ergebnis erhalten wir eine Sammlung von Paaren, und die Zeile beschreibt die angewendeten Änderungen, und das zweite Element des Paares ist das Objekt, in dem alle diese Änderungen kombiniert werden.


Basierend auf der Struktur des Modells des getesteten Codes in Form eines Baums stellen die Änderungslisten den Pfad von der Wurzel zu den Blättern dieses Baums dar. Somit wird ein wesentlicher Teil der Änderungen dupliziert. Sie können diese Duplizierung mithilfe der DSL-Option entfernen, bei der die Änderungen direkt auf das Basisobjekt angewendet werden, wenn Sie sich entlang der Zweige bewegen. In diesem Fall werden weniger unnötige Berechnungen durchgeführt.


Automatische Generierung von Testdaten


Da es sich um eine weiße Box handelt, können wir alle Zweige sehen. Auf diese Weise können Sie ein Logikmodell erstellen, das in einer weißen Box enthalten ist, und das Modell zum Generieren von Testdaten verwenden. Wenn der Testcode in Scala geschrieben ist, können Sie beispielsweise Scalameta zum Lesen des Codes verwenden und anschließend in ein Logikmodell konvertieren. Wie in der zuvor diskutierten Ausgabe der Modellierung der Logik von Veränderungen ist es auch hier schwierig für uns, alle Möglichkeiten einer universellen Sprache zu modellieren. Ferner nehmen wir an, dass der getestete Code unter Verwendung einer begrenzten Teilmenge der Sprache oder in einer anderen Sprache oder DSL implementiert wird, die anfänglich begrenzt ist. Dadurch können wir uns auf die Aspekte der Sprache konzentrieren, die für uns von Interesse sind.


Betrachten Sie ein Beispiel für Code, der einen einzelnen Zweig enthält:


 if(object.field1 == "123") A else B 

Die Bedingung teilt die Menge der field1 Werte in zwei Äquivalenzklassen auf: == "123" und != "123" . Daher wird der gesamte Satz von Eingabedaten in Bezug auf diese Bedingung auch in zwei Äquivalenzklassen unterteilt - ClassCondition1IsTrue und ClassCondition1IsFalse . Unter dem Gesichtspunkt der Vollständigkeit der Berichterstattung reicht es aus, mindestens ein Beispiel aus diesen beiden Klassen zu nehmen, um beide Zweige A und B abzudecken B Für die erste Klasse können wir ein Beispiel in gewisser Weise auf einzigartige Weise field1 : Nehmen Sie ein zufälliges Objekt, aber ändern Sie field1 in "123" . Darüber hinaus wird das Objekt sicherlich in der Äquivalenzklasse ClassCondition1IsTrue und die Berechnungen werden entlang Zweig A Es gibt weitere Beispiele für die zweite Klasse. Eine Möglichkeit, ein Beispiel für die zweite Klasse zu generieren, besteht darin, beliebige Eingabeobjekte zu generieren und diese mit field1 == "123" verwerfen. Eine andere Möglichkeit: field1 ein zufälliges Objekt zu nehmen, aber field1 in "123" + "*" ändern (zur Änderung können Sie jede Änderung in der Steuerzeile verwenden, um sicherzustellen, dass die neue Zeile nicht der Steuerzeile entspricht).


Arbitrary und Gen aus der ScalaCheck-Bibliothek eignen sich gut als Zufallsdatengeneratoren.


Im Wesentlichen rufen wir die in der if verwendete boolesche Funktion auf. Das heißt, wir finden alle Werte des Eingabeobjekts, für das diese Boolesche Funktion true - ClassCondition1IsTrue , und alle Werte des Eingabeobjekts, für das false - ClassCondition1IsFalse akzeptiert ClassCondition1IsFalse .


In ähnlicher Weise ist es möglich, Daten zu generieren, die für die Einschränkungen geeignet sind, die von einfachen bedingten Operatoren mit Konstanten generiert werden (mehr / weniger als eine in einer Menge enthaltene Konstante beginnt mit einer Konstanten). Solche Bedingungen sind leicht umzukehren. Selbst wenn einfache Funktionen im Testcode aufgerufen werden, können wir ihren Aufruf durch ihre Definition (Inline) ersetzen und dennoch bedingte Ausdrücke invertieren.


Harte reversible Funktionen


Die Situation ist anders, wenn die Bedingung eine Funktion verwendet, die schwer umzukehren ist. Wenn beispielsweise eine Hash-Funktion verwendet wird, scheint es nicht möglich zu sein, automatisch ein Beispiel mit dem gewünschten Wert des Hash-Codes zu generieren.


In diesem Fall können Sie dem Eingabeobjekt, das das Ergebnis der Funktionsberechnung darstellt, einen zusätzlichen Parameter hinzufügen, den Funktionsaufruf durch einen Aufruf dieses Parameters ersetzen und diesen Parameter trotz Verletzung der Funktionsverbindung aktualisieren:


 if(sha(object.field1)=="a9403...") ... //     ==> if(object.sha_field1 == "a9403...") ... 

Ein zusätzlicher Parameter ermöglicht die Codeausführung innerhalb des Zweigs, kann jedoch offensichtlich zu tatsächlich falschen Ergebnissen führen. Das heißt, das Testprogramm liefert Ergebnisse, die in der Realität niemals beobachtet werden können. Die Überprüfung eines Teils des Codes, auf den wir sonst nicht zugreifen können, ist dennoch nützlich und kann als eine Form des Komponententests angesehen werden. Schließlich wird auch während des Komponententests eine Unterfunktion mit Argumenten aufgerufen, die möglicherweise nie im Programm verwendet werden.


Mit solchen Manipulationen ersetzen wir das Testobjekt. In gewissem Sinne enthält das neu erstellte Programm jedoch die Logik des alten Programms. Wenn wir als Werte der neuen künstlichen Parameter die Ergebnisse der Berechnung der Funktionen verwenden, die wir durch die Parameter ersetzt haben, liefert das Programm dieselben Ergebnisse. Anscheinend könnte das Testen des modifizierten Programms immer noch von Interesse sein. Sie müssen sich nur merken, unter welchen Bedingungen sich das geänderte Programm genauso verhält wie das ursprüngliche.


Abhängige Bedingungen


, . , , , . , . (, , x > 0 , — x <= 1 . , — (-∞, 0] , (0, 1] , (1, +∞) , — .)


, , , true false . , , " " .



, , :


 if(x > 0) if(y > 0) if (y > x) 

( > 0 , — y > x .)
"", , , , , . , " " .
, "", ( y == x + 1 ), , .
"" ( y > x + 1 && y < x + 2 ), , .



, , - , "c " ( Symbolic Execution , ), . ( field1 = field1_initial_value ). , . :


 val a = field1 + 10 //    a = field_initial_value + 10 val b = a * 3 //    b = 3 * field_initial_value + 30 

true false . . . Zum Beispiel


 if(a > 0) A else B //   A ,  field_initial_value + 10 > 0 //   B ,  field_initial_value + 10 <= 0 

, , , , . , (, , ).



. , , . , . , .


, . . , , . , , , . , , , , , ?


Y- ( " " , stackoverflow:What is a Y-combinator? (2- ) , habr: Y- 7 ). , . ( , , .) . , . , "" . Y- " " ( ).


( ). , . , . , . , , , TestCase '. , , ( throw Nothing bottom , ). .


, . . , , . , . , , . , , , , . , . , .



, , , , 100% . , , . Hm. . , , , ? , , - .


:


  1. .
  2. ( ).
  3. , .
  4. , .

, , . -, , , , . -, , , ( , ), , , "" . / , .


Fazit


" " " ". , , , , . , .


, , , , . -, , ( ), . -, -, . DSL, , . -, , . -, , ( , , ). .


, , . , , - .


Danksagung


@mneychev .

Source: https://habr.com/ru/post/de422283/


All Articles