[JAVA] Nur daran möchte ich mich erinnern, 4 Entwürfe zum Schreiben von Unit-Tests

Ich habe auch etwas mehr Konventionen in "Ich habe versucht, die UT-Erstellungskriterien in Java zu organisieren" zusammengefasst. ..

Einführung

Letztes Jahr schrieb ich einen Artikel im Blog mit dem Titel "7 Regeln für normale Unit-Tests". Es gab mehr Reaktionen als ich erwartet hatte. Was ich in diesem Artikel geschrieben habe, ist nur das Prinzip / Prinzip, und einige Techniken sind erforderlich, um es zu realisieren.

Insbesondere, selbst wenn Sie eine solche Regel festlegen und sich strikt an das "Schreiben eines Komponententests" halten. Ohne Kenntnis der richtigen Technik kann die Wartung schwierig sein, die Qualität wird möglicherweise nicht verbessert, und Müll mit schlechter Ausführungsleistung kann in Massenproduktion hergestellt werden.

Dann müssen Sie "den Originalcode von Anfang an so gestalten, dass der Komponententest einfach zu schreiben ist". damit. Das erste, was Sie lernen sollten, ist nicht "wie man Testcode schreibt", sondern "zu testender Code", dh "wie man Produktcode schreibt". Außerdem ist das hier erwähnte "von Anfang an" eine Stufe, die Sie ordnungsgemäß ausführen können, bevor Sie es Personen wie Review und PR-Zusammenführung geben, ohne gründlich zu sagen: "Es ist Test zuerst! Machen Sie TDD!" ..

Da es sich jedoch um eine geringfügige Änderung des Denkens handelt, denke ich, dass es für Anfänger einfacher ist, ein konkretes Beispiel zu sehen, und habe daher ein typisches Beispiel als Spickzettel erstellt.

Design Spickzettel

Die Grundidee besteht darin, "Geschäftslogik und E / A zu trennen" und "denselben Wert zurückzugeben, unabhängig davon, wie oft er wiederholt wird". Das Folgende ist ein konkretes Beispiel, das auf dieser Idee basiert.

Ich möchte ein Programm erstellen, das Standard ausgibt

Testen Sie eine Methode, die einfach eine Zeichenfolge zurückgibt und die Ausgabe, z. B. "System.out.println" und "Logger.info", vom Teil "Assembler the output" trennt.

Beispiel für einen Produktcode:

public class Example01Good {
    public static void main(String[] args) {
        String message = args[0];
        System.out.println(makeMessage(message));
    }

    static String makeMessage(String message) {
        return "Hello, " + message;
    }
}

Beispiel für einen Testcode:

@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}

Ich möchte eine Methode erstellen, die Zufallszahlen verarbeitet

Da sich der Wert einer Zufallszahl jedes Mal ändert, übergeben Sie den Wert als Argument getrennt von der Geschäftslogik. Ein Programm, das bestimmt, ob die Würfel gerade oder ungerade sind, sieht so aus.

Beispiel für einen Produktcode:

public class Example02Good {
    public static void main(String[] args) {
        System.out.println(check(throwDice(Math.random())));
    }

    static String check(int num) {
        return (num % 2 == 0) ? "Win" : "Lose";
    }

    static int throwDice(double rand) {
        return (int) (rand * 6);
    }
}

Beispiel für einen Testcode:

@Test
public void testCheck() {
    assertThat(Example02Good.check(1), is("Lose"));
    assertThat(Example02Good.check(2), is("Win"));
    assertThat(Example02Good.check(3), is("Lose"));
    assertThat(Example02Good.check(4), is("Win"));
    assertThat(Example02Good.check(5), is("Lose"));
    assertThat(Example02Good.check(6), is("Win"));
}

Ich möchte eine Methode erstellen, die Daten wie die Berechnung des nächsten Tages behandelt

Machen Sie es von außen wie bei Zufallszahlen. Erstellen Sie als weitere Lösung wie im Beispiel eine Factory zur Datumsgenerierung und verwenden Sie zum Testen einen Dummy (Stub).

Beispiel für einen Produktcode:

class SystemDate {
    public LocalDate today() {
        return LocalDate.now();
    }
}

public class Example03Good {
    SystemDate systemDate = new SystemDate();

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}

Beispiel für einen Testcode:

@Test
public void testTomorrow2() {
    Example03Good target = new Example03Good();
    target.systemDate = new SystemDate() {
        @Override
        public LocalDate today() {
            return LocalDate.of(2017, 1, 16);
        }
    };
    assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}

Ich möchte die Eingabe / Ausgabe von Dateien behandeln

Grundsätzlich sollte die Dateieingabe / -ausgabe eine Methode sein, die Zeichenfolgen verarbeitet, aber Es gibt Fälle, in denen die tatsächlichen Daten sehr groß sind oder als "Zeilen" verarbeitet werden müssen. In diesem Fall können Sie das Lesen und Schreiben der Datei tatsächlich überprüfen. Da jedoch die Beschreibungs- und Ausführungskosten hoch sind, Trennen Sie die direkte Behandlung von Dateien innerhalb der Logik, z. B. Reader / Writer und InputStream / OutputStream Es ist einfach, die Schnittstelle zu programmieren und StringReader / StringWriter usw. zum Testen zu verwenden.

Beispiel für einen Produktcode:

public class Example04Good {
    public static void main(String[] args) throws Exception {
        System.out.println("hello");
        try (Reader reader = Files.newBufferedReader(Paths.get("intput.txt"));
                Writer writer = Files.newBufferedWriter(Paths.get("output.txt"));) {
            addLineNumber(reader, writer);
        }
    }

    static void addLineNumber(Reader reader, Writer writer) throws IOException {
        try (BufferedReader br = new BufferedReader(reader);
                PrintWriter pw = new PrintWriter(writer);) {
            int i = 1;
            for (String line = br.readLine(); line != null; line = br.readLine()) {
                pw.println(i + ": " + line);
                i += 1;
            }
        }
    }
}

Beispiel für einen Testcode:

@Test
public void testAddLineNumber() throws Exception {
    Writer writer = new StringWriter();
    addLineNumber(new StringReader("a\nb\nc\n"), writer);
    writer.flush();
    String[] actuals = writer.toString().split(System.lineSeparator());

    assertThat(actuals.length, is(3));
    assertThat(actuals[0], is("1: a"));
    assertThat(actuals[1], is("2: b"));
    assertThat(actuals[2], is("3: c"));
}

Kommentar oder Grundidee

Warum das schreiben, nicht nur ein konkretes Beispiel? Ich werde das auch erklären.

Trennung von Logik und E / A.

Auch hier sind die Grundlagen die Trennung von Logik und I / O. Hier sind einige Tipps zum Testen. Nehmen wir als Beispiel "ein Programm, das von Befehlszeilenargumenten empfangene Zeichen verarbeitet und standardmäßig ausgibt".

public class Example01Bad {
    public static void main(String[] args) {
        String message = args[0];
//         String message = "World"; //Zur Funktionsprüfung
        System.out.println("Hello, " + message);
    }
}

Ist dies nicht der erste Code, den viele Leute schreiben? Der auskommentierte Code ist nett. Schreiben wir einen Unit-Test für dieses Programm.

@Test
public void testMain() {
    String[] args = {"world"};
    Example01Bad.main(args);
}

Ist es so Es gibt keine Behauptung! Also, ob dieses Programm normal ist oder nicht, ist ** "Menschen müssen visuell beurteilen" **, obwohl es JUnit ist.

Sie könnten denken: "Niemand schreibt diese Art von Code, www.", Aber ich habe diese Art von "Testcode, der einfach funktioniert" oft mit komplizierterer Geschäftslogik gesehen.

Nun, das kommt nicht in Frage, aber eine etwas bewusstere Person schreibt:

/**
 *Hochbewusster System nutzloser Test
 */
@Test
public void testMainWithSystemOut() {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    System.setOut(new PrintStream(out));

    String[] args = {"world"};
    Example01Bad.main(args);

    assertThat(out.toString(), is("Hello, world" + System.lineSeparator()));
}

Ich hänge die Standardausgabe an und vergleiche die Ergebnisse. Dieser Test erfüllt die Anforderungen des Tests ordnungsgemäß, nichts ist falsch, aber es ist einfach nutzlos kompliziert. Ich werde es also einfacher machen, indem ich den Originalcode ändere.

public class Example01Good {
    public static void main(String[] args) {
        String message = args[0];
        System.out.println(makeMessage(message));
    }

    static String makeMessage(String message) {
        return "Hello, " + message;
    }

"Logik zum Verarbeiten von Zeichenketten" wurde als makeMessage ausgeschnitten. Dann sieht der Unit-Test so aus.

@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}

Es ist sehr einfach geworden, nicht wahr? Es fühlt sich gut an.

Einige Leute haben jedoch möglicherweise das Gefühl, dass "ich die Funktion der Anzeige von Zeichen auf dem Bildschirm nicht testen konnte!" Korrekt. Es besteht jedoch keine Notwendigkeit, dies durch einen Komponententest zu bestätigen.

Der wichtigste Komponententest, auf den Sie sich konzentrieren sollten, ist der Logiktest. Es sind keine bereits gut bestätigten Tests wie Dateieingabe / -ausgabe, Standardeingabe / -ausgabe oder Protokollausgabe erforderlich.

Wenn Sie eine solche Datei IO etc. machen und deren Qualität nicht garantiert ist, Da die Qualität durch Testen der von Ihnen erstellten Datei-E / A garantiert werden sollte, ist es nicht erforderlich, diese durch eine einzelne Verarbeitung zu überprüfen, die die Funktion verwendet. Das Testen in einer solchen Kombination ist ebenfalls wichtig, wird jedoch in einer anderen Phase wie dem Integrationstest durchgeführt.

Daher ist es wichtig, sich stets der Erstellung einer Geschäftslogik bewusst zu sein, die einfach den Rückgabewert zurückgibt, indem die Funktion wie diesmal so klein wie möglich unterteilt wird. Es wird empfohlen, E / A und Initialisierung der unten beschriebenen unkontrollierbaren Werte so weit wie möglich auf die Controller-Ebene zu übertragen und die dortigen Tests aus dem UT zu entfernen, der bei jeder Ausführung des Builds ausgeführt wird. Für die Pflege von Legacy-Code kann es unvermeidlich sein, die Standardausgabe wie im fehlerhaften Beispiel gezeigt zu verknüpfen, aber es ist nicht erforderlich, wenn Sie sie selbst schreiben.

Initialisieren Sie keine Dinge direkt, für die Sie den Wert nicht steuern können

Es ist auch wichtig, unkontrollierbare Dinge wie Datumsangaben, Zufallszahlen oder Netzwerke (wie das Aufrufen von WebAPI) in der getesteten Methode als typisches Beispiel nicht zu initialisieren.

Erstellen Sie beispielsweise eine Morgenmethode, die nach Morgen fragt.

public class Example03Bad {
    public LocalDate tomorrow() {
        return LocalDate.now().plusDays(1);
    }
}

Wenn Sie LocalDate direkt in der Methode von morgen wie folgt initialisieren, ändert sich der Wert natürlich jeden Tag, sodass ein automatisches Testen nicht möglich ist.

Dies ist ein ziemlich ernstes Problem, und einige Leute sagen: "Ich weiß nicht, wie man einen Komponententest schreibt", weil ich diese Abhängigkeiten nicht richtig beseitigt habe.

Die einfachste Lösung besteht darin, das Datum als Argument zu übergeben und es von der Geschäftslogik zu trennen.

public LocalDate tomorrow(LocalDate today) {
    return today.plusDays(1);
}

Dann kann der Test mit einem festen Wert wie diesem geschrieben werden.

@Test
public void testTomorrow() {
    Example03Good target = new Example03Good();
    assertThat(target.tomorrow(LocalDate.of(2017, 1, 16)), is(LocalDate.of(2017, 1, 17)));
}

Grundsätzlich ist dies in Ordnung, aber manchmal ist es einfacher, das Factory-Methodenmuster und die Stubs zu verwenden, wenn es viele davon gibt. Erstellen Sie zunächst eine SystemDate-Klasse, die LocalDate generiert, verwenden Sie LocalDate.now und legen Sie sie im Feld Example03Good fest. Die Geschäftslogik ruft das LocalDate über die SystemDate-Klasse ab. Was zurückgegeben wird, hängt also von der Implementierung ab. Der Produktcode lautet LocalDate.now.

class SystemDate {
    public LocalDate today() {
        return LocalDate.now();
    }
}

public class Example03Good {
    SystemDate systemDate = new SystemDate();

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}

Der Testcode überschreibt systemDate mit einem solchen Stub.

@Test
public void testTomorrow2() {
    Example03Good target = new Example03Good();
    target.systemDate = new SystemDate() {
        @Override
        public LocalDate today() {
            return LocalDate.of(2017, 1, 16);
        }
    };
    assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}

Der Trick dabei ist, Factory-Felder wie SystemDate auf Paketebene oder höher und nicht privat zu platzieren. Anschließend können Sie Implementierungen wechseln, ohne einen DI-Container oder ein Mock-Framework zu verwenden. Natürlich ist es in Ordnung, es privat zu machen und im Konstruktor zu übergeben oder im Setter zu packen, aber am Ende ist es immer noch möglich, es zu ändern, also ist es einfach besser, einfach zu sein.

Zusammenfassung

Nun, ich habe es kurz geschrieben, aber wie ist es? Ich hoffe, Sie haben festgestellt, dass ein wenig Design-Einfallsreichtum das Schreiben des Tests erheblich erleichtert.

Es ist nicht schwierig, aber ich mache Anfängern oft den gleichen Punkt, und ich kannte das Material, das aus dieser Sicht zusammengefasst wird, nicht, also habe ich es geschrieben. Übrigens heißt es, dass "TDD (testgetriebene Entwicklung) zur Qualität beiträgt", weil diese Art des Schreibens natürlich erzwungen wird.

Auch beim Lernen einer funktionalen Sprache gibt es einige Punkte, die bei solchen Entwurfsmethoden als ultimativ erscheinen. Ich denke, es gibt viele Punkte, die hilfreich sein können.

Dann auch dieses Jahr Happy Hacking!

Recommended Posts

Nur daran möchte ich mich erinnern, 4 Entwürfe zum Schreiben von Unit-Tests
Ich möchte einen Unit Test schreiben!
[Rails] So implementieren Sie einen Unit-Test eines Modells
Ich möchte ContainerRelativeShape nur auf bestimmte Ecken anwenden [SwiftUI]
Ich möchte eine generische Anmerkung für einen Typ erstellen
Ich möchte beim Schreiben von Testcode zufällig Informationen generieren
Ich möchte mehrere Rückgabewerte für das eingegebene Argument zurückgeben
Ich möchte Zeichen konvertieren ...
Ich möchte rekursiv nach Dateien in einem bestimmten Verzeichnis suchen
Ich möchte nur dem Poster Bearbeitungs- und Löschberechtigungen erteilen
Die Geschichte von Collectors.groupingBy, die ich für die Nachwelt behalten möchte
[Ruby] Ich möchte nur das ungerade Zeichen in der Zeichenfolge ausgeben
Wenn Rails eine Sitzung für einen bestimmten Controller deaktivieren soll
Glassfish Tuning List, die ich vorerst behalten möchte
[Ruby] Ich möchte nur den Wert des Hash und nur den Schlüssel extrahieren
[Rspec] Ablauf von der Einführung von Rspec bis zum Schreiben von Unit-Test-Code für das Modell
Ich möchte, dass Sie Enum # name () für den Schlüssel von SharedPreference verwenden