Alter Hund lehrt neue Tricks: Code Kata mit QuickCheck

    Wenn ich Programmiererkollegen auffordere, mehr verschiedene Selbsttests für ihren Code zu erstellen, beklagen sie sich häufig darüber, dass dies eine komplizierte und langweilige Aufgabe ist. Und in gewisser Hinsicht haben sie recht. Tatsächlich müssen Sie bei der Verwendung klassischer Komponententests häufig viel Code schreiben, um jeden einzelnen Verhaltensfall zu überprüfen. Ja, und die Qualität des Testens wirft manchmal Fragen auf, insbesondere in komplexen Systemen, wenn triviale Anwendungsfälle einen Knall auslösen, aber in einigen komplexeren Szenarien, für die niemand daran gedacht hat , Tests zu schreiben, treten unangenehme Probleme auf.

    Ich habe lange von der in QuickCheck verwendeten Testmethode gehört , aber es fehlte immer noch der letzte Schubs, um sie richtig zu machen. Dieser Schub war die Präsentation. von John Hughes, Autor dieser wundervollen Bibliothek.

    Was ist der QuickCheck-Ansatz?


    Das Wesen des Ansatzes lässt sich ganz einfach beschreiben: Wir erstellen keine Testbeispiele, sondern legen die Regeln fest , die das Verhalten des Systems auf beliebigen Eingabedaten bestimmen . Die Bibliothek selbst generiert eine große Anzahl zufälliger Eingabedaten und prüft, ob das Codeverhalten den festgelegten Regeln entspricht. Ist dies nicht der Fall, zeigt es uns, auf welches Beispiel der Test fällt.

    Klingt das vielversprechend? Ganz.


    Das ist genau die richtige Seite, um dieses Wunder einem einfachen Programmierer nahe zu bringeneine Person, die nicht in Haskell und nicht in Erlang schreibt, sondern in den gängigsten Sprachen? Zum Beispiel fühle ich mich wohler beim Programmieren in Java. Kein problem Google schlägt fast sofort vor, dass es für JUnit ein entsprechendes Plugin namens JUnit-QuickCheck gibt .

    Die beste Möglichkeit, einen neuen Programmieransatz auszuprobieren, besteht darin, etwas bereits Bekanntes zu schreiben. Also habe ich die klassische Prime Factors Kata von Robert Martin genommen . Ich empfehle, dass Sie sich schnell damit vertraut machen, bevor Sie meinen Artikel lesen, da sonst einige Punkte möglicherweise nicht klar sind.

    Lass uns gehen


    Erstellen Sie zunächst ein leeres Projekt. Um den Leser nicht mit einem Bogen XML-Dateien zu langweilen, werde ich dafür Gradle verwenden . Damit passt die gesamte Beschreibung des Projekts in mehrere Zeilen:

    apply plugin: 'java'
    repositories {
        mavenCentral()
    }
    dependencies {
        testCompile (
            "junit:junit:4.11",
            "org.hamcrest:hamcrest-all:1.3",
            "org.junit.contrib:junit-theories:4.11",
            "com.pholser:junit-quickcheck-core:0.4-beta-1",
            "com.pholser:junit-quickcheck-generators:0.4-beta-1"
        )
    }
    


    Jede Sucht ist hier nicht zufällig. Es ist nicht nötig zu erklären, warum JUnit hier benötigt wird, aber ich werde ein paar Worte über den Rest der Abhängigkeiten sagen.

    • Wir werden Hamcrest verwenden, um schöne und leicht lesbare Aussagen zu schreiben.
    • JUnit-Theories ist auch notwendig, weil unser Plugin nur damit funktioniert (weil der unangenehme Fehler in der in JUnit eingebauten Version der Theorien noch nicht behoben wurde )
    • Die Projekte junit-quickcheck-core und junit-quickcheck-generators enthalten Klassen, mit denen wir direkt Testwerte generieren.


    Nach den Prinzipien von TDD beginnen wir mit der einfachsten Theorie, mit der Sie schnell sicherstellen können, dass die Rückkopplungsschleife funktioniert.

    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;
    import com.pholser.junit.quickcheck.ForAll;
    import org.junit.contrib.theories.Theories;
    import org.junit.contrib.theories.Theory;
    import org.junit.runner.RunWith;
    @RunWith(Theories.class)
    public class PrimeFactorsTest
    {
        @Theory public void allOk(@ForAll Integer number) {
            assertThat(true, is(true));
        }
    }
    


    Dieser einfache Trick kann Ihnen viel Zeit sparen. Ich habe oft gesehen, dass Benutzer von TDD viel Zeit damit verbringen, einen komplexen Test zu erstellen, und als sie ihn schließlich ausführen, stellen sie fest, dass er aufgrund eines völlig irrelevanten Problems nicht funktioniert (Abhängigkeiten wurden nicht heruntergeladen oder registriert, JDK ist installiert, das Projekt ist falsch konfiguriert, der Code ist falsch geschrieben und viele andere lächerliche Fehler. Es ist immer sehr frustrierend und verwirrend für den Arbeitsrhythmus. Es ist besonders schwierig, mit diesen Neulingen umzugehen, die immer noch versuchen, TDD zu probieren.

    Deshalb beginne ich selbst immer mit dem einfachsten, trivialsten, schwachsinnigsten Test, und ich rate Ihnen, dasselbe zu tun. Sie müssen es nur ausführen und überprüfen, was ich sehe, wenn es passiert, und sehen, wann es fällt. Das heißt, mein System ist bereit fürKampfarbeit , und nichts wird vom Rot-Grün-Refaktor-Zyklus ablenken.

    Erste Arbeitstheorie


    Um mich nicht mit der Frage zu beschäftigen, wie man Primzahlen identifiziert (mein Code sollte das tun!), Hämmere ich einfach die bereits bekannten Zahlen in ein Array . Offensichtlich sind wir aufgrund der Beschränkungen unserer Liste auch gezwungen, den Bereich der zu testenden Zahlen zu begrenzen. Wir werden das später beheben, wenn es soweit ist. Um nicht von der Hauptsache abgelenkt zu werden, werde ich keine Importe mehr in den Code schreiben, ich werde mich nur noch auf den Code selbst beschränken.

    @Theory public void primeNumberIsItsOwnFactor(@ForAll @InRange(minInt = 1, maxInt = 50) Integer number) {
        List firstPrimeNumbers = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47);
        assumeThat(number, isIn(firstPrimeNumbers));
        List factors = PrimeFactors.extract(number);
        assertThat(factors, hasItem(number));
    }
    


    Wir verwenden Anmerkungen @ForAllund @InRangedas Projekt JUnit-Quick Check, um automatisch eine Zufallszahl in einem bestimmten Intervall zu erzeugen. Dann filtern wir sie zusätzlich mit der Funktion assumeThat, so dass der nachfolgende Code nur mit den Zahlen funktioniert , die ich im Array angegeben habe. Der Unterschied zwischen den assumeThatvon assertThatder Tatsache , dass die erste Funktion hält nur den Test (und bewegt sich zu dem nächsten Beispiel), wenn die nächste Nummer nicht getestet wird, und die zweite wird ein Fehlersignal (und die Ausführung des Tests für alle folgenden Beispiele Stop). Die Verwendung von assume in Tests ist idiomatischer als das Filtern von Werten mit bedingten Ausdrücken.

    Dieser Test wird zuerst fallen (da wir keine Methodenimplementierung haben)extract), aber es ist leicht zu beheben. Die Lösung, die alle Tests besteht, ist trivial.

    public class PrimeFactors
    {
        public static List extract(Integer number)
        {
            return Arrays.asList(number);
        }
    }
    


    Seien Sie nicht überrascht oder empört im Voraus. Dieser Code arbeitet vollständig in Übereinstimmung mit der Spezifikation und zerlegt jede Primzahl, die 50 nicht überschreitet , in Primfaktoren . Um dem Code beizubringen, wie man mit anderen Zahlen arbeitet, schreiben Sie einfach eine neue Theorie.

    Bauen Sie Fleisch auf ein Skelett


    Welche Eigenschaften hat die Menge der Zahlenfaktoren? Offensichtlich muss ihr Produkt der Zahl selbst entsprechen.

    @Theory public void productOfFactorsShouldBeEqualToNumber(@ForAll @InRange(minInt = 2, maxInt = 50) Integer number) {
        List factors = PrimeFactors.extract(number);
        Integer product = 1;
        for (Integer factor: factors)
            product = product * factor;
        assertThat(product, is(number));
    }
    


    Diese Theorie ... fällt nicht! Und tatsächlich, wenn unser Code die Nummer selbst zurückgibt, wird es immer so sein. Hölle

    Okay, eine andere Theorie, diesmal erfolgreicher.

    @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = 50) Integer number) {
        List factors = PrimeFactors.extract(number);
        assertThat(factors, everyItem(isIn(firstPrimeNumbers)));
    }
    


    Jeder Faktor sollte einfach sein (siehe Liste der einfachen Faktoren), damit die Theorie stabil und regelmäßig abfällt. Und genau das brauchen wir. Angenommen, der folgende Fehler tritt auf:

    org.junit.contrib.theories.internal.ParameterizedAssertionError: everyFactorShouldBeSimple("10" )
        at org.junit.contrib.theories.Theories$TheoryAnchor.reportParameterizedError(Theories.java:215)
        at org.junit.contrib.theories.Theories$TheoryAnchor$1$1.evaluate(Theories.java:169)
        at org.junit.contrib.theories.Theories$TheoryAnchor.runWithCompleteAssignment(Theories.java:153)
        at org.junit.contrib.theories.Theories$TheoryAnchor.runWithAssignment(Theories.java:142)
        ...
    


    Schreiben wir den einfachsten Code, mit dem Sie die Teiler für diese Zahl finden können:

    public class PrimeFactors
    {
        public static List extract(Integer number)
        {
            if (number % 2 == 0)
                return Arrays.asList(2, number / 2);
            return Arrays.asList(number);
        }
    }
    


    Führen Sie die Tests erneut aus. Sie fallen automatisch auf den neu gefundenen Wert:

    org.junit.contrib.theories.internal.ParameterizedAssertionError: everyFactorShouldBeSimple("15" )
        at org.junit.contrib.theories.Theories$TheoryAnchor.reportParameterizedError(Theories.java:215)
        at org.junit.contrib.theories.Theories$TheoryAnchor$1$1.evaluate(Theories.java:169)
        at org.junit.contrib.theories.Theories$TheoryAnchor.runWithCompleteAssignment(Theories.java:153)
        ...
    


    Machen wir auch einen Hack für ihn!

    public class PrimeFactors
    {
        public static List extract(Integer number)
        {
            if (number % 2 == 0)
                return Arrays.asList(2, number / 2);
            if (number % 3 == 0)
                return Arrays.asList(3, number / 3);
            return Arrays.asList(number);
        }
    }
    


    Wir führen die Tests immer wieder durch und finden jedes Mal einen neuen Wert, auf den die Implementierung fällt. Wir können jedoch nicht einfach eine Primzahl zurückgeben, damit der Test erfolgreich ist. Wenn wir dies tun, beginnt die vorherige Theorie (die das Produkt der Zahlen überprüft) zusammenzubrechen. Deshalb müssen wir Schritt für Schritt den richtigen Algorithmus implementieren.

    Allmählich (und tatsächlich ziemlich schnell) führt uns diese Reihe von Hacks zur ersten richtigen Entscheidung .

    public class PrimeFactors
    {
        public static List extract(Integer number)
        {
            List factors = new ArrayList<>();
            for (int divisor = 2; divisor <=7; divisor++) {
                while ((number > divisor) && (number % divisor == 0)) {
                    factors.add(divisor);
                    number = number / divisor;
                }
            }
            factors.add(number);
            return factors;
        }
    }
    


    Natürlich bedeutet das Wort "richtige Entscheidung" nur, dass es alle Tests in diesem Stadium stabil besteht . Obwohl es offensichtlich nicht für den allgemeinen Fall geeignet ist.

    Sie sollten eine Pause machen und ein wenig nachdenken. Die Theorie, die selbst ein Gegenbeispiel für den aktuellen Code auswählt , erweist sich als sehr praktisch. Die Bearbeitung des Codes verwandelt sich in ein Ping-Pong-Spiel mit einem Roboter, der schnelle, genaue und knifflige Schläge liefert. Sie müssen keine Zeit damit verbringen, über neue Beispiele nachzudenken, die Code brechen, weil sie von selbst geboren werden. Stattdessen können Sie sich ganz auf die Überlegungen zum Algorithmus konzentrieren und ihn im Flow-Modus bearbeiten. Dies erklärt teilweise, warum es einen so großen Sprung bei den Commits gab.. Es ist nur so, dass der Code zu schnell geboren wurde, als dass sich die Zwischenschritte verhärten und zu vollständigen Commits formen ließen.

    Bisher scheint alles sehr cool zu sein. Wir haben nur ein paar Theorien geschrieben und sie haben unseren Algorithmus halbautomatisch weiterentwickelt. Ist das nicht schön? Mal sehen, was als nächstes passiert.

    Es ist Zeit, aus kurzen Hosen herauszuwachsen


    Die Euphorie verschwindet allmählich und das Auge fängt an, auf die scharfen Ecken zu achten, die wir in den frühen Stadien sorgfältig eingekreist haben. Unser Code funktioniert natürlich gemäß der Spezifikation, aber diese Spezifikation ist nur für Zahlen von 2 bis 50 definiert. Nicht reich! In diesem Intervall können Sie auf ein Programm verzichten, zählen Sie einfach alles in Ihrem Kopf.

    Lass uns weitermachen. Erhöhen Sie die Obergrenze in allen Theorien um das Zehnfache!

    @Theory public void primeNumberIsItsOwnFactor(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) {
        ...
    }
    @Theory public void productOfFactorsShouldBeEqualToNumber(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) {
        ...
    }
    @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) {
        ...
    }
    


    Plötzlich tritt ein neues Problem auf: Unseren Theorien ist nicht bewusst, dass es mehr als 47 Primzahlen gibt (oops, niemand hat sie mit Euklid bekannt gemacht ). Wir müssen einen neuen Weg finden, um Primzahlen zu bestimmen.

    Wir betrügen ein wenig (oder ist alles ehrlich?) Und verwenden die vorgefertigte Simplicity-Implementierung , die in der Standard-Java-Bibliothek enthalten ist. Um die Schönheit und Einheitlichkeit des Codes nicht zu verletzen, werden wir ihn in Form eines entsprechenden Matchers erstellen.

    @Theory public void primeNumberIsItsOwnFactor(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) {
        assumeThat(number, isProbablySimple());
        List factors = PrimeFactors.extract(number);
        assertThat(factors, hasItem(number));
    }
    @Theory public void productOfFactorsShouldBeEqualToNumber(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) {
        ...
    }
    @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = 500) Integer number) {
        List factors = PrimeFactors.extract(number);
        assertThat(factors, everyItem(isProbablySimple()));
    }
    private Matcher isProbablySimple()
    {
        return new BaseMatcher()
        {
            @Override
            public boolean matches(Object item)
            {
                return (item instanceof Integer) &&
                    (BigInteger.valueOf((Integer) item).isProbablePrime(5));
            }
            @Override
            public void describeTo(Description description)
            {
                description.appendText("prime number");
            }
        };
    }
    


    Jetzt fällt unser Code auf die Zerlegung großer Zahlen. Es ist Zeit, es zu beheben!

    public class PrimeFactors
    {
        public static List extract(Integer number)
        {
            List factors = new ArrayList<>();
            for (int divisor = 2; divisor <= number; divisor++) {
                ...
    


    Wir fixieren die alte Schleifengrenze (7) numberund alles scheint wieder zu funktionieren.

    Es bleibt nur noch wenig übrig: die Grenzen der Tests noch weiter zu erweitern und das Ergebnis zu genießen. Und dann erwartet uns eine plötzliche Überraschung ...

    Angesichts der harten Realität


    Die Realität sieht so aus :

    @Theory public void primeNumberIsItsOwnFactor(@ForAll @InRange(minInt = 2, maxInt = Integer.MAX_VALUE) Integer number) {
        ...
    }
    @Theory public void productOfFactorsShouldBeEqualToNumber(@ForAll @InRange(minInt = 2, maxInt = Integer.MAX_VALUE) Integer number) {
        ...
    }
    @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = Integer.MAX_VALUE) Integer number) {
        ...
    }
    


    Sobald wir die Obergrenze der Tests von 500 auf Integer.MAX_VALUE(2 ^ 31 - 1) erhöht hatten , begann der Test unrealistisch lange zu funktionieren . Eine Minute für jeden Test. Was ist los Was ist los?

    Ein unerwarteter Nebeneffekt von QuickCheck-Tests ist die Empfindlichkeit gegenüber der Geschwindigkeit des getesteten Codes . Wenn Sie darüber nachdenken, ist dies jedoch logisch: Wenn unser Code nicht optimiert ist und langsam läuft, werden hundert Aufrufe diese Nichtoptimalität hundertmal sichtbarer. In den "klassischen" Unit-Tests wird diese Verlangsamung nicht so spürbar sein, aber hier zeigt sie sich in all ihrer Pracht.

    Was machen wir, wenn wir einen Stecker im Code finden müssen? Es gibt zwei Möglichkeiten: Entweder nehmen wir den Profiler in die Hand und beginnen mit dem Ablesen, oder wir suchen anhand der Prüfmethode nach einem Fehler .

    In unserem Code gibt es jedoch nichts Besonderes zu sehen, alles ist in Sichtweite. Das Problem ist, dass wir zu lange durch den Kreis gefahren sind und vergeblich Strom verschwendet haben. Jeder, der mit dem Faktorisierungsalgorithmus vertraut ist, merkt sich, dass es ausreicht, Faktoren zu überprüfen, die die Quadratwurzel einer bestimmten Zahl nicht überschreiten. Und wer sich nicht erinnert, kann zu Onkel Bob gehen .

    Wir werden den Fix ausführen . Ändern Sie erneut die obere Grenze der Schleife, diesmal jedoch in Math.sqrt(number).

    public class PrimeFactors
    {
        public static List extract(Integer number)
        {
            List factors = new ArrayList<>();
            for (int divisor = 2; divisor <= Math.sqrt(number) + 1; divisor++) {
                ...
    


    Wie hat sich das auf das Ergebnis der Arbeit ausgewirkt? Die Tests begannen wieder schnell zu funktionieren und der Unterschied ist wirklich beeindruckend.

    Jetzt ist schon alles in Ordnung! Alle Tests sind bestanden, der Code sieht ordentlich aus, es wurde eine interessante Erfahrung gesammelt - ist es an der Zeit, einen Artikel über Habr zu schreiben? Und dann schleicht sich ein anderer Gedanke in meinen Kopf ...

    Testen Sie Ihre Tests


    Halt, mein Freund, sag ich mir, hast du die Randbedingung des Zyklus richtig aufgeschrieben? Ist es wirklich notwendig, eine zur Wurzel der Zahl hinzuzufügen, oder ist dies überflüssig?

    Es scheint eine unbedeutende Frage zu sein. Aber wir haben Tests, die mit hundert Testwerten laufen! Sie werden zeigen, wer hier falsch liegt.

    Subtrahiere "+1" oben in der Schleife ( divisor <= Math.sqrt(number);) und führe die Tests aus.

    Großartig, sie bestehen!

    Wir nehmen einfach so eine weitere Einheit, nur für den Fall ( divisor < Math.sqrt(number);).

    Tests bestehen erneut!

    Was denn

    Und hier musste ich noch einmal überlegen. Verschlimmert die Situation noch weiter .

    public class PrimeFactors
    {
        public static List extract(Integer number)
        {
            List factors = new ArrayList<>();
            for (int divisor = 2; divisor < Math.sqrt(number) - 2; divisor++) {
                ...
    


    Ich habe offensichtlich einen falschen Code geschrieben (Multiplikatoren werden auch in Nummer 9 nicht gefunden), aber Tests bestätigen, dass alles in Ordnung ist . Ich fange sie wieder an - sie sagen wieder, dass alles in Ordnung ist. Ich fange sie wieder an - immer wieder gehen sie erfolgreich vorbei. Stürze treten sehr selten auf und Gegenbeispiele zu meinem fehlerhaften Algorithmus, die Tests gelegentlich finden, werden nicht für zukünftige Läufe gespeichert.

    Was ist der Grund für dieses Testverhalten?

    Durch die Erweiterung der Testgrenzen auf Integer.MAX_VALUEkonnten wir Leistungsprobleme finden und beheben, sind jedoch in eine neue Falle geraten. Der Trick ist, dass mit diesen Reichweiteneinstellungen in den Tests meistens großeZahlen (da für die Generierung eine gleichmäßige Verteilung verwendet wird). Und der in den Code eingeführte Fehler manifestiert sich nur auf den Quadraten von Primzahlen (ich hoffe, es bedarf keiner Erklärung), die unter großen Zahlen sehr wenige sind .

    Leider war es mir nicht möglich, Lösungen zu finden, die erfolgreicher waren, als noch einmal ein wenig zu schummeln und eine Kopie der vorhandenen Spezifikation anzufertigen, sondern nur noch einmal mit schmalen Rändern.

    @Theory public void everyFactorShouldBeSimple(@ForAll @InRange(minInt = 2, maxInt = Integer.MAX_VALUE) Integer number) {
        List factors = PrimeFactors.extract(number);
        assertThat(factors, everyItem(isProbablySimple()));
    }
    @Theory public void everyFactorShouldBeSimpleEspeciallyForSmallNumbers(@ForAll @InRange(minInt = 2, maxInt = 200) Integer number) {
        everyFactorShouldBeSimple(number);
    }
    


    Es sieht ungeschickt aus, aber es ermöglicht Ihnen zumindest, die genaue Obergrenze zu finden, bis zu der Sie das Fahrrad fahren müssen ( divisor <= Math.sqrt(number)).

    Es ist Zeit, Bilanz zu ziehen und alle Entdeckungen zusammenzuführen, die uns in diesem (scheinbar) einfachen Beispiel begegnet sind.

    Was haben wir als Ergebnis bekommen?


    Selbst ein Experiment in einem unbekannten Gebiet kann viele Entdeckungen bringen. Ich werde versuchen, alle Funktionen des QuickCheck-Ansatzes in einem Bundle zusammenzufassen und auszuwerten.

    Lakonische Spezifikation



    In der Tat gibt es so etwas. Ich musste nur drei Theorien schreiben, von denen jede eine Eigenschaft des Algorithmus testete. Dies ist deutlich weniger als ein Dutzend konventioneller Unit-Tests, die in der klassischen Version der Kata durchgeführt werden. Wir schreiben diese Funktion in einem bestimmten Plus dieser Technik.

    Die Notwendigkeit, überprüfbare Eigenschaften sorgfältig zu formulieren



    Damit Theorien gut funktionieren, müssen qualitative Eigenschaften für die Verifikation gefunden werden, die in Bezug auf die Eingabeparameter unveränderlich sind. Manchmal kann es sehr kompliziert sein. Möglicherweise müssen Sie den Testalgorithmus vollständig im Testcode implementieren.

    Im obigen Beispiel war es möglich, ein Verfahren zu verwenden isProbablePrime, das der Einfachheit halber einen schnellen heuristischen Algorithmus zur ungenauen Überprüfung von Zahlen verwendet. Wenn es einen solchen Algorithmus nicht geben würde, welche Überprüfungsoptionen bieten wir dann an? Tatsächlich ist eine Primzahl per Definition eine Zahl ohne Teiler. Um die Einfachheit der Zahl zu überprüfen, müssen Sie versuchen , sie in Teiler zu unterteilen .

    Dies ist wahrscheinlich der schwierigste Moment beim Testen von QuickCheck. Ich werde weitere Untersuchungen benötigen, um zu verstehen, wie schwierig es ist, gute Invarianten für die Verwendung in Theorien zu erstellen.

    Langsame Codeempfindlichkeit



    Einerseits ist das gut, weil es sofort darauf hindeuten kann, dass unser Code nicht optimal ist. Wenn der Code im Prinzip nicht stark beschleunigt werden kann, müssen Sie entweder die langsame Ausführung der Tests akzeptieren oder die Anzahl der Zufallswerte reduzieren, die als Testparameter verwendet werden. Und wenn wir die Anzahl der Zufallswerte verringern, sinkt unsere Gewissheit, dass Tests mögliche Fehler im Code finden, in angemessenem Maße.

    Ich denke, Sie haben bereits vermutet, dass die Verwendung von QuickCheck für End-to-End-Tests aus diesem Grund möglicherweise nicht die beste Idee ist. Obwohl, wenn Sie wirklich wollen, dann können Sie es versuchen .

    Unempfindlichkeit gegenüber Randbedingungen



    Möglicherweise ist dies eine Besonderheit der JUnit-QuickCheck-Bibliothek, in anderen Sprachen ist dies jedoch besser. Oder ist es ein Merkmal des spezifischen Beispiels, das wir für den Test ausgewählt haben? Dies zeigt jedoch, dass Sie sich nicht immer leichtfertig auf zufällige Werte verlassen sollten, die die Bibliothek für uns hilfreich auswählt. Trotzdem müssen Sie mit Ihrem Gehirn gründlich nachdenken und die Richtigkeit des geschriebenen Codes überprüfen.

    QuickCheck kann auch für TDD verwendet werden!



    Es scheint ziemlich real zu sein, obwohl die Empfindungen unterschiedlich sind. Aufgrund der Tatsache, dass es weniger Theorien gibt (von denen jede mehr Fälle testet), ist es einfacher, eine Kette von Testmethoden zu erstellen, die uns zu einem funktionierenden Code führen. Auf der anderen Seite kann dies zu Problemen führen, wenn Sie einen zu großen Schritt unternehmen müssen, damit der Code eine neu hinzugefügte Theorie durchläuft. Menschen stoßen jedoch bei der klassischen TDD auf solche Probleme (und finden entweder Möglichkeiten, sie zu lösen, oder sie fürchten sich im Prinzip vor der TDD).

    Es ist möglich, dass beim Testen des Codes eine Kombination aus klassischen Testfällen und parametrisierten Theorien im QuickCheck-Stil gut funktioniert. Ich werde auf jeden Fall versuchen, meine Forschung auf diesem Gebiet fortzusetzen und interessante Erkenntnisse auszutauschen.

    Jetzt auch beliebt: