Unit-Testing

in Arbeit ...

1. Allgemeines

Siehe auch Unit-Testing mit BlueJ (1. Klasse).

Testen bedeutet allgemein die Überprüfung, ob sich etwas so verhält, wie es spezifiziert ist oder erwartet wird.

Da jede Code-Änderung potentiell neue Fehler verursachen kann, ist häufiges und möglichst vollständiges Testen extrem wichtig für die Software-Qualität.

Sehr viele Fehler lassen sich schon auf "niedrigstem Niveau" prüfen. Hier werden einzelne Methoden oder kleinstmögliche Gruppen von Methoden getestet – das Unit Testing.

Manche Fehler sind erst im Zusammenspiel größerer funktioneller Einheiten erkennbar – das Integration Testing

Prüfung auf Fehler bei der tatsächlichen Nutzung erfordern die Prüfung des Zusammenspiels des gesamten Systems – das System Testing

2. JUnit5

Maven erzeugt standardmäßig einerseits den "normalen" Source Folder src/main/java für den "produktiven" Source-Code und andererseits src/test/java fur den Code der Tests.
In src/main bzw. src/test kann es jeweils neben Ordner "java" (u.a.) auch den Ordner "resources" geben, der statische Materialien wie Bilder, Audio- und Video-Dateien, Sylesheets, Templates, Konfigurationsdateien und vieles mehr enthalten kann.

Sehr häufig werden die Testklassen in die selbe Package-Hierarchieposition wie die Dateien der src-Packages abgelegt. Da somit die jeweilige Testklasse im selben Package wie die zu testende liegt, können auch Elemente mit Sichtbarkeit protected oder package (ohne Sichtbarkeits-Bezeichner!) getestet werden.

2.1. Annotations zur Steuerung

Folgende Annotations werden verwendet, um die Aufgabe diverser Methoden etc. zu definieren:

  • @Test …​ markiert eine Test-Methode

  • @BeforeAll …​ markiert eine statische (bis auf Sonderfälle) Methode, die ganz zu Beginn vor allen Tests ausgeführt wird

  • @AfterAll …​ markiert eine statische (bis auf Sonderfälle) Methode, die ganz am Ende nach allen Tests ausgeführt wird

  • @BeforeEach …​ markiert eine Methode, die vor JEDEM Test ausgeführt wird

  • @AfterEach …​ markiert eine Methode, die nach JEDEM Test ausgeführt wird

  • @Disabled("Info-Text") …​ markiert eine Methode, die nicht ausgeführt werden soll

  • Weitere Annotations wie z.B. @EnabledOnOs({ LINUX, MAC }), @DisabledOnJre(JAVA_9) u.a. siehe JUnit 5 User Guide: Annotations

2.2. Einige Regeln und Aspekte

  • Nach einem Fehler sollten die verbleibenden Tests weiterlaufen. Da bei Fehlschlag normalerweise die betroffene Test-Methode verlassen wird, ist dies nicht automatisch der Fall.

    Daher sollte als einfachster Ansatz nur ein einziger Test pro Methode ausgeführt werden oder aber man nutzt einen sogenannten → im besten Fall nur ein einziger (oder sehr wenige) Test(s) je Methode oder Nutzung z.B. von JUnit5 @ParameterizedTest, AssertAll()

  • Am einfachsten ist Testen für Rückgabewerte von Methodenaufrufen.

  • Änderungen von Objekt-Attributen sind einfach testbar, wenn ein Getter dazu verfügbar ist.

  • Das Werfen von Exceptions kann mit assertThrows(..) einfach getestet werden.

  • Objekt-Parameter, die innerhalb einer Methode geändert werden, sind auch außerhalb der Methode geändert und können somit getestet werden. U.a. daher ist das Ändern von Objekt-Parametern innerhalb einer Methode oft nicht empfehlenswert.

  • Ausgaben auf der Konsole sind etwas mühsam zu testen - es muss der Ausgabe-Datenstrom geändert werden. Daher ist es meist besser, die Ausgabe in einem String zu sammeln und diesen zu testen.

    Notfalls kann man wie nachfolgend gezeigt vorgehen.

Testen von Ausgaben auf der Konsole:
    /** **************************************************************************
     * Here we have 2 constants and 2 methods which allow to:
     * 1. 'catch' the standard output and store  it into a buffer
     * 2. take the output (suck) from buffer, provide it as a String (finally clean up change)
     * Usage: before the code for which we want to take the stdout into our force,
     *    call {@code bendStdout2Buffer() }, then run the relevant piece of code, then
     *    call {@code suckStdoutBufferAndReset() } and use returned String containing the output.
     *  That's all!
     */
    public void bendStdout2Buffer() {
        PrintStream myPs = new PrintStream(BAOS);
        System.setOut(myPs);
    }

    private static final PrintStream OUT_STREAM_ORIG = System.out;
    private static final ByteArrayOutputStream BAOS = new ByteArrayOutputStream();

    public String suckStdoutBufferAndReset() {
        if (System.out == OUT_STREAM_ORIG) {
            throw new IllegalStateException("System.out not bent");
        }
        String content = BAOS.toString();
        BAOS.reset();
        System.setOut(OUT_STREAM_ORIG);
        return content;
    }
    // ^^^^^^^^^^^^ end of the output catching methods and constants ^^^^^^^^^^^^----

3. Testen von GUIs mit JavaFX und von Web-Apps

Für JavaFX existiert das Framework TestFX, für Web-Browser das Browser-Automatisierungs-Framework Selenium. Beides kann sehr gut mit JUnit kombiniert werden.

4.1. Allgemein

JUnit 5 User Guide …​ 2022-12-29

A JUnit Tutorial | Toptal …​ 31.12.2022, 12:17:00

What is Software Testing? Definition …​ 8.1.2023, 11:46:20

A Guide to JUnit 5 | Baeldung …​ 8.1.2023, 11:58:08

https://medium.com/javarevisited/getting-started-with-junit-5-part-2-multiple-test-4495aebf958b [Getting Started Testing with JUnit 5: Part 2 (Multiple Test) | by Aziz Kale | Javarevisited | Medium] …​ 2024-01-02

https://www.bairesdev.com/blog/java-unit-testing/ [Java Unit Testing With JUnit 5: Best Practices & Techniques Explained | Blog - BairesDev] …​ 2024-01-02

https://www.vogella.com/tutorials/JUnit/article.html [JUnit 5 tutorial - Learn how to write unit tests] …​ 2024-01-02

https://www.baeldung.com/junit-5-repeated-test [A Guide to @RepeatedTest in Junit 5 | Baeldung] …​ 2024-01-02

4.2. Unit-Tests konkret

Die geläufigsten Test-Methoden sind assertEquals(..), assertTrue(..), assertFalse(..), assertNull(..), assertNotNull(..).

    @Test
    void testSomething() {
        assertEquals(3, "Hey".length());
        assertTrue(true || false);
        assertFalse(true && false);
        assertNull(null);
        assertNotNull("not null");
    }

Wenn mehrere "Asserts" in einem Test vorkommen, endet die Methode, sobald der erste Test fehlschlägt. Dies ist oft nicht erwünscht, da so nicht alle Fehler angezeigt werden. Daher gibt es die Möglichkeit, die Tests mit einem "AssertAll" zu umhüllen. Damit werden alle Tests ausgeführt:

Alle Tests auch bei Fehlschlägen ausführen:
    @Test
    void testSeveral() {
        assertAll("Several tests within one method",
                () -> assertEquals(33, "Hey".length()),  // Fehlschlag
                () -> assertTrue(true || false),
                () -> assertFalse(true && false),
                () -> assertNull("xxx"),                 // Fehlschlag
                () -> assertNotNull("not null")
        );
    }

Methode assertAll hat optional als ersten Parameter einen Titel-String, danach eine variable Parameterliste von Assert-Methoden assertXXX(…​).
Im Prinzip enthält die Liste Executable-Objekte. Ein Executable-Objekt ist eine funktionale Schnittstelle, die eine Methode void execute() definiert. Die assertXXX-Methoden sind statische Methoden, die ein solches Executable-Objekt zurückgeben.

Exceptions können mit assertThrows(..) getestet werden:
import static org.junit.jupiter.api.Assertions.assertThrows;

    User user = new User();  // class with attribute "age" of type Integer

    @Test
    void testException() {
        assertThrows(NullPointerException.class, () -> {
            String s = null;
            s.length();
        });
    }

    @Test
    void testExceptionMessage() {
        // set up user object attribute before!
        Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
            user.setAge("23");  // erroneously using a string instead of an integer
        });
        assertEquals("Age must be an Integer.", exception.getMessage());
    }
TODO: AssertAll

4.4. Integration und System Tests

4.6. Weiteres