Zum Inhalt

Testing #

Um sicherzustellen, dass eine Methode oder sogar eine ganze Applikation korrekt funktioniert, muss sie getestet werden. Da Applikationen immer grösser und komplexer werden, wird das manuelle Testen zeitaufwendiger. Um dieser Entwicklung entgegenzuwirken, werden automatische Tests geschrieben, welche das Testen übernehmen.

Das Ziel von Testing#

Das Ziel von Testen ist es, die Funktionalität einer Applikation sicherzustellen. Wenn eine Änderung vorgenommen wird, dann sollte automatisch validiert werden, ob immer noch alles so funktioniert wie es sollte.

Was wird getestet?#

In der Theorie kann jede Interaktion, die mit der Applikation gemacht wird, getestet werden. Je nach Umfang werden unterschiedlich viele Komponenten automatisiert getestet, da die Instandhaltung von den Tests nicht einfach ist. Heutzutage ist es Standard, dass einfache Methoden automatisiert getestet werden. Eine Übersicht, wie ein Entwicklungsprozess mit Tests ablaufen könnte, ist im folgenden Bild zu erkennen:

DevOps Ablauf DevOps Ablauf

Integration Testing#

Integrationstests werten eine Applikation in einem grösseren Bereich als Unit Tests aus. Das bedeutet, es werden mehrere Funktionalitäten gleichzeitig getestet, um sicherzustellen, dass die Zusammenarbeit davon reibungslos verläuft. Es wird zum Beispiel getestet, ob eine Datenbank oder ein Dateisystem richtig eingebunden wurde. Diese Tests haben meistens eine längere Ausführungszeit, da sie einen grossen Teil des Codes testen.

In Spring Boot können diese Tests anhand von API-Abfragen ermöglicht werden. Dabei wird die API abgefragt und die Rückgabewerte validiert. Um statische Werte zu generieren, werden Mocks verwendet, was zum Schluss noch genauer erläutert wird. Ein ausserordentlich gutes Tutorial zum Thema Integration testen einer REST-API findet man auf Baeldung.

Load / Stress Testing#

Load und Stress Testen sind wichtig, um sicherzustellen, dass eine Applikation performant und skalierbar ist. Das Ziel von beiden Tests ist unterschiedlich, auch wenn sie ähnliche Tests beinhalten.

Load: Testet, wie stabil eine Applikation ist, wenn sie auf einem hohen Anwendungsvolumen läuft. So sollte sichergestellt werden, dass auch ganz viele Benutzer gleichzeitig auf der Applikation arbeiten können oder grosse Datenmengen unterstützt werden.

Stress: Testet wie stabil eine Applikation auf extremen Anwendungsvolumen läuft, oft für eine lange Zeitperiode. Die Tests belasten die Anwendung mit einer Belastungsspitze, einer allmählichen ansteigender Last oder mit begrenzten Rechenressourcen. Es sollte sichergestellt werden, dass sich von Fehlern erholt und zur normalen Funktion zurückgekehrt werden kann.

UI Testing#

Um sicherzustellen, dass die Benutzeroberfläche einer Applikation korrekt funktioniert, werden die Benutzeroberflächen getestet. Dies kann bei einer HTML-Seite zum Beispiel mit Selenium erfolgen. Es wird dabei ein Ablauf oder eine Tätigkeit schrittweise durchlaufen. Nach jeder Tätigkeit wird geprüft, ob die richtigen Werte gesetzt wurden und ob das Ziel erreichbar ist. So kann zum Beispiel sichergestellt werden, dass jeder Knopf immer eine Funktionalität hat.

Das UI ist jedoch etwas, was sehr oft manuell getestet wird, da das Schreiben und Instandhalten dieser Abläufe aufwendig ist. Zudem kann nicht erkannt werden, ob die Elemente korrekt aussehen und ob die Benutzerführung angenehm ist.

Beschreibungen#

Test Beispiel
Unit-Testing Methode liefert korrekte Rückgabewerte
Integration-Testing Produkt kann auf einer Webseite gekauft werden
System-Integration-Testing Installation & Inbetriebnahme erfolgreich
Load / Stress-Testing Berechnung X kann Y Mal pro Sekunde ausgeführt werden
Abnahme / Nutzungs-Testing Zusammenarbeit mit anderen Softwares / Microservice

Was sind Unit Tests?#

Unit Tests sind kleine Tests, die einzelne Methoden testen. Es werden möglichst kleine Aufgaben getestet und deren Ergebnisse validiert. Das Ziel davon ist es, dass die Applikation ständig automatisiert getestet wird. Eine Methode wird dabei mit verschiedenen Grenzwerten ausgeführt. Die Rückgabe wird dann validiert. Es sollte ungefähr 80% aller Methoden mithilfe von Unit Tests getestet werden. So sollten Probleme frühzeitig erkannt werden. Diese Tests gehen nur wenige Millisekunden, weswegen sie im Hintergrund ausgeführt werden können. Eine verständliche Beschreibung zu guten Tests kann auf MS Docs gefunden werden.

Vorteile#

Sicherheit: Es kann sichergestellt werden, dass eine Änderung keine ungewollten Nebeneffekte erzeugt.

Schnell: Es ist ungewöhnlich, dass ein Projekt Tausende von Tests hat. Unit Tests dauern nur wenige Millisekunden.

Unabhängig: Damit Code einfach getestet werden kann, darf es nicht zu starken Verbindungen kommen. Dies zwingt natürliche und unabhängige Methoden zu schreiben.

Dokumentation: Durch Tests wird das Verhalten dokumentiert und veranschaulicht. So sieht man, wie auf verschiedene Werte reagiert wird.

Automatisiert: Die Tests werden automatisch durchgelaufen. Es ist somit nicht nötig, die Methode von Hand zu testen.

Tests erstellen#

In den folgenden Abschnitten wird erklärt, wo und wie Unit Tests erstellt werden. Diese Erklärung wird mithilfe von JUnit 5 gemacht, da dieses Framework zu einem Standard in Java wurde. Andere Frameworks verhalten sich jedoch fast gleich.

Maven Installation

JUnit kann mithilfe von Maven installiert und verwaltet werden:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>{VERSION}</version>
    <scope>test</scope>
</dependency>

Damit die Tests ausgeführt werden können, braucht es noch ein Plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M3</version>
</plugin>

Wo wird getestet#

Der Sourcecode wird im main-Ordner geschrieben. Die Tests werden im test-Ordner geschrieben.

Dateistruktur
src:.
├───main
│   ├───java
│   │   └───ch
│   │       └───bztf
│   │           └───m226blb1 #(1)
│   │               ├───Calculator.java
│   │               └───Application.java
│   └───resources
│       └───application.yml
└───test
    └───java
        └───ch
            └───bztf
                └───m226blb1 #(2)
                    └───CalculatorTest.java
  1. Hier unter dem Pfad (\main) wird der Applikationscode geschrieben. Die Klasse Calculator wird in diese Beispiel verwendet.

  2. Hier unter dem Pfad (\test) werden die Tests geschrieben. Üblicherweise haben sie denselben Namen wie die Klasse, die getestet wird. Als Suffix wird Test angehängt, damit die Dateien sofort erkannt werden.

Ausführen & Ausgaben#

Tests können auf unterschiedliche Weise ausgeführt werden. Der Grund dafür ist, dass je nach Anwendungszweck und Umgebung andere Voraussetzungen zu erfüllen sind.

Maven#

Maven kann sehr viele Aufgaben automatisieren. Unter anderem das Ausführen von Tests, was mithilfe folgendem Befehl gemacht werden kann. Nach dem Ausführen sollte ein Text in der Konsole ausgegeben werden.

mvn test
# Applikationsstart...

[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 #(1)
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  22.050 s
[INFO] Finished at: 2022-05-15T23:18:53+02:00 #(2)
[INFO] ------------------------------------------------------------------------
  1. Die Anzahl der ausgeführten Tests und wie viele davon einen Fehler hatten.
  2. Das Datum zur Ausführung und die benötigte Zeit

Visual Studio Code#

Alle modernen IDE's können Tests ausführen. Visual Studio Code besitzt diese Funktion über eine Extension auch. Ein Tutorial, wie man das ganze aufsetzt, kann auf der offiziellen Webseite gefunden werden. Unter dem Tab Testing können die Resultate der einzelnen Tests angeschaut werden. Dort besteht zudem auch die Option, alle Tests oder nur einen Spezifischen erneut durchzuführen. Wenn es zu einem Fehler kommt, dann wird der Verlauf der Tests dargestellt, damit man sieht, wann es zuletzt funktionierte.

Test Beispiel

  1. Mithilfe des Knopfes mit den zwei Pfeilen bei der Klasse werden alle Tests ausgeführt. Dies wird gebraucht, um die volle Funktionalität zu testen.
  2. Wenn nur ein Test geprüft werden möchte, dann kann der grüne Knopf neben der Methode verwendet werden.

Tipp

Wenn ein Test fehlgeschlagen ist, dann ist dieser hier sehr schnell auffindbar. Mithilfe eines Rechtsklicks auf den Test kann dann gleich zur Klasse navigiert werden. Es lohnt sich vor einem Commit diese Tests auszuführen und zu überprüfen, ob alle fehlerfrei durchlaufen. Dies dauert im Normalfall nur wenige Sekunden, welche jedoch einen schönen Git Verlauf ermöglichen.

Test Resultat in VS-Code

Test Resultat in VS-Code

GitHub Actions#

Tests sollten immer wieder ausgeführt werden. Wer mit Source Control arbeitet, kann dies automatisch machen. Mithilfe von GitHub Actions können Tests je nach Ereignis und Commits ausgeführt werden. Um dies zu erreichen, muss eine Datei im Ordner .github/workflows erstellt werden.

Die Datei braucht folgenden Inhalt:

.github/workflows/ci-tests.yml
name: ci-tests #(1)
on:
  push:
    branches: #(2)
      - main
jobs:
  run_tests:
    runs-on: ubuntu-latest #(3)
    steps:
      - name: Checkout the repository
        uses: actions/checkout@v3 #(4)

      - name: Set up JDK 18
        uses: actions/setup-java@v1 #(5)
        with:
          java-version: 18

      - name: Cache Maven packages
        uses: actions/cache@v3 #(6)
        with:
          path: ~/.m2
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-m2

      - name: Run tests with Maven #(7)
        run: mvn -B verify --file pom.xml
  1. Der Name der Aktion der in der Übersicht dargestellt wird.
  2. Der Branch, auf den die Aktion ausgeführt wird. Damit kann verhindert werden, dass die Tests auf Entwicklungsbranches ausgeführt werden, welche noch Fehler besitzen.
  3. Die Maschine, auf welcher die Tests ausgeführt werden sollen. So können verschiedene Betriebssysteme getestet werden.
  4. Die Commits müssen zuerst auf das aktuelle Repository geladen werden, bevor die Tests ausgeführt werden. Ansonsten wird der vorherige Code getestet.
  5. Die Java Version, welche benutzt werden soll. Es sollte dieselbe Version wie in der IDE verwendet werden.
  6. Die aktuellen Maven dependencies werden in einem Cache gespeichert. So kann die Startzeit der Tests drastisch verringert werden.
  7. Hier werden die Tests und Maven Abhängigkeiten ausgeführt und getestet.

Bei jedem Commit wird ein ❌ für gescheitert oder ein für erfolgreich angehängt. Im Verlauf kann angeschaut werden, welche Commits erfolgreich waren und welche scheiterten.

GitHub Actions Test Resultat

Einfaches Beispiel#

In den vorherigen Abschnitten wurde bereits erklärt, wo die Tests geschrieben werden und wie sie aufgebaut sind.

Dies ist ein einfaches Beispiel eines Unit Tests. Mithilfe von Assert wird ein Wert überprüft. Falls dieser nicht dem erwarteten Wert entspricht, dann wird ein Fehler ausgegeben. Es gibt viele verschiedene Arten von Assert, mit welchen unterschiedliche Tests durchgeführt werden können. Die Methoden haben gleich wie die Klasse den Suffix Test.

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = Calculator.class) //(1)
public class CalculatorTest {
    @Test
    public void addSameValuesTest() {
        assertEquals(4, Calculator.add(2, 2)); //(2)
    }

    @Test
    public void addHighTest() {
        assertEquals(314_159_265, Calculator.add(104_719_755, 209_439_510)); //(3)
    }

    @Test
    public void addMinusTest() {
        assertEquals(-2, Calculator.add(-4, 2)); //(4)
    }
}
  1. Hier wird angegeben welche Klasse für den Test benötigt wird. Dies ist nicht immer nötig, kann jedoch Helfen falls nicht alle Klassen richtig geladen werden.
  2. Dieser Test testet noch nicht alle Funktionalitäten.
    Da 2 + 2 = 4 aber auch 2 * 2 = 4 ist.
  3. Es ist wichtig, dass Extremfälle wie dieser hier getestet wird. So kann sichergestellt werden, dass auch mit grosen Zahlen gerechnet werden kann.
  4. Zu Extremfällen gehören auch negative Werte.

Eine simple Methode, welche zwei Ganzzahlen addiert.

public class Calculator {
  public static int add(int a, int b) {
      return a + b;
  }
}

Was ist Mocking#

Im vorherigen Beispiel wurde nur eine einfache Methode, welche keine Abhängigkeit hat, getestet. Dies ist nicht immer der Fall. Meistens ist noch eine Datenbank oder ein API-Abruf eingebunden. Die Rückgabewerte können sich ändern, was das erwartete Ergebnis verhindert. Aus diesem Grund sollten Tests so gut wie möglich isoliert sein. Dies wird durch Mocking1 erreicht. So kann sichergestellt werden, dass da Problem nur in der aktuellen Methode liegt. Dabei die Abhängigkeit überschrieben, sodass statische Werte zurückgegeben werden. Wenn Mocking verwendet wird, dann ist meistens die Frage, ob die Tests nicht bereits integrations Tests sind.

Mocking Maven Installation

Mocking kann mithilfe des Frameworks Mockito gemacht werden.

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>

Komplexes Beispiel#

Nicht immer ist es der Fall, dass eine Methode dieselben Werte zurückgibt. Durch Mocking kann dieses Problem gelöst werden. Dazu muss ein Feld mit der Annotation @Mock versehen werden.

Der WinkeService könnte auch eine Datenbank benutzen, welche die Winke zurückgibt. Diese Datenbank kann aber nicht im Test verändert werden. Diese Datenbank muss also im Test als Mock verwendet werden. Einen Schritt weiter wäre es, wenn eine Testdatenbank erstellt und diese mit vordefinierten Werten getestet wird. Dies wäre jedoch etwas zu weit für einen einfachen Unit Test.

Gleich wie beim einfachen Beispiel wird eine Methode aufgerufen. Dann wird überprüft, ob die Methode den erwarteten Wert zurückgibt. Über das Symbol können weitere Infos angezeigt werden.

Dies ist ein etwas komplizierterer Test, welcher Mocks verwendet und mehrere Klassen gleichzeitig testet. Was die ganzen Begriffe und Annotationen bedeuten, kann auch hier anhand von Beispielen gefunden werden.

@InjectMocks //(1)
WinkeController winkeController = new WinkeController();

@Mock //(2)
WinkeService winkeService;

@Test
public void getWinkeTest() {
    // Setup mock (3)
    when(winkeService.getWinke()).thenReturn("Winke 1 Mal");

    // Test (4)
    String result = winkeController.getWinkeText();

    // Assert (5)
    assertEquals("Winke 1 Mal", result);
}
  1. Mithilfe von @InjectMocks wird versucht ein Objekt mithilfe von Dependency Injection einzubinden. Es ist somit equivalent wie das @Autowired.
  2. Mithilfe der Annotation Mock wird Mockito mitgeteilt, dass eine Klasse gemockt werden sollte. Alle Klassen, welche eine Abhängigkeit von dieser Klasse haben werden nun diesen Mock verwenden.
  3. Ein Mock besteht aus zwei Teilen. when und thenReturn. when übernimmt die Methode, welche gemockt werden sollte. thenReturn gibt den statischen Rückgabewert zurück.
  4. Hier wird die Methode wie gewohnt aufgerufen. Dabei wird im Hintergrund der Mock verwendet.
  5. Der Rückgabewert wird mit dem erwarteten Ergebnis verglichen. Da die Methode gemockt wurde wird immer derselbe Wert zurückgegeben.

Diese Klasse gibt einen String zurück, welcher die Anzahl der Winke anzeigt. Der String beinhaltet jedoch ein Zufallswert, was das einfache Testen des Rückgabewertes verhindert.

@Service
public class WinkeService {
    public String getWinke() {
        return "Winke " + Math.random() + " Mal";
    }
}

Der Controller hat eine Verbindung zum WinkeService, welcher die Methode getWinke() beinhaltet. Der Service wird automatisch über @Autowired geladen. Beim Aufruf der API wird dann der Text zurückgegeben, wie viel Mal gewunken wurde.

@RestController
@RequestMapping("/")
public class WinkeController {
    @Autowired
    private WinkeService winkeService;

    @GetMapping("/winke")
    public String getWinkeText() {
        return winkeService.getWinke();
    }
}

Beide Beispiele sind ausführbar auf GitHub zu finden: test/Beispiele


  1. Mocking kommt aus dem Englischen und bedeutet so viel wie Nachahmen.