
Arten von Tests
Was ist ein Test? Laut Wikipedia: „Softwaretests umfassen die Ausführung einer Softwarekomponente oder Systemkomponente, um eine oder mehrere interessierende Eigenschaften zu bewerten.“ Mit anderen Worten: Es handelt sich um eine Überprüfung der Korrektheit unseres Systems in bestimmten Situationen. Schauen wir uns mal an, welche Arten von Tests es im Allgemeinen gibt:- Unit-Tests – Tests, deren Zweck darin besteht, jedes Modul des Systems separat zu überprüfen. Diese Tests sollten für die kleinsten atomaren Teile des Systems gelten, z. B. Module.
- Systemtests – Tests auf hoher Ebene, um den Betrieb eines größeren Teils der Anwendung oder des Systems als Ganzes zu überprüfen.
- Regressionstests – Tests, mit denen überprüft wird, ob neue Funktionen oder Fehlerbehebungen die vorhandene Funktionalität der Anwendung beeinträchtigen oder alte Fehler einführen.
- Funktionstests – Überprüfung, ob ein Teil der Anwendung die in den Spezifikationen, User Stories usw. genannten Anforderungen erfüllt.
Arten von Funktionstests:
- White-Box-Tests – Überprüfen, ob ein Teil der Anwendung die Anforderungen erfüllt, während gleichzeitig die interne Implementierung des Systems bekannt ist;
- Black-Box-Tests – Überprüfen, ob ein Teil der Anwendung die Anforderungen erfüllt, ohne die interne Implementierung des Systems zu kennen.
- Leistungstests – Tests, die geschrieben werden, um zu bestimmen, wie sich das System oder ein Teil des Systems unter einer bestimmten Last verhält.
- Lasttests – Tests, mit denen die Stabilität des Systems unter Standardlasten überprüft und die maximale Last ermittelt werden soll, bei der die Anwendung noch ordnungsgemäß funktioniert.
- Stresstests – Tests zur Überprüfung der Leistung der Anwendung unter nicht standardmäßigen Belastungen und zur Bestimmung der maximalen Belastung vor einem Systemausfall.
- Sicherheitstests – Tests zur Überprüfung der Systemsicherheit (vor Hackern, Viren, unbefugtem Zugriff auf vertrauliche Daten und anderen gefährlichen Angriffen).
- Lokalisierungstests – Tests der Lokalisierung der Anwendung.
- Usability-Tests – Tests zur Überprüfung der Benutzerfreundlichkeit, Verständlichkeit, Attraktivität und Erlernbarkeit.

- Unit – Dieser Abschnitt bezieht sich auf Unit-Tests, die in verschiedenen Schichten der Anwendung angewendet werden. Sie testen die kleinste teilbare Einheit der Anwendungslogik. Zum Beispiel Klassen, am häufigsten jedoch Methoden. Bei diesen Tests wird in der Regel versucht, das Getestete so weit wie möglich von jeglicher externen Logik zu isolieren. Das heißt, sie versuchen, die Illusion zu erzeugen, dass der Rest der Anwendung wie erwartet läuft.
Von diesen Tests sollte es immer viele geben (mehr als bei jedem anderen Typ), da sie kleine Teile testen und sehr leichtgewichtig sind und nicht viele Ressourcen (d. h. RAM und Zeit) verbrauchen.
- Integration – Dieser Abschnitt bezieht sich auf Integrationstests. Bei diesem Test werden größere Teile des Systems überprüft. Das heißt, es kombiniert entweder mehrere Logikelemente (mehrere Methoden oder Klassen) oder prüft die Korrektheit der Interaktion mit einer externen Komponente. Diese Tests sind normalerweise kleiner als Unit-Tests, weil sie schwerer sind.
Ein Beispiel für einen Integrationstest könnte darin bestehen, eine Verbindung zu einer Datenbank herzustellen und die Korrektheit der Funktionsweise von Methoden für die Arbeit damit zu überprüfen.
- Benutzeroberfläche – Dieser Abschnitt bezieht sich auf Tests, die den Betrieb der Benutzeroberfläche überprüfen. Sie beziehen die Logik auf allen Ebenen der Anwendung ein, weshalb sie auch End-to-End-Tests genannt werden. Davon gibt es in der Regel deutlich weniger, da sie am umständlichsten sind und die notwendigsten (benutzten) Wege prüfen müssen.
Im Bild oben sehen wir, dass die verschiedenen Teile des Dreiecks unterschiedlich groß sind: Bei der Anzahl der verschiedenen Arten von Tests in der realen Arbeit bestehen ungefähr die gleichen Proportionen.
Heute werfen wir einen genaueren Blick auf die gängigsten Tests, Unit-Tests, da alle Java-Entwickler mit etwas Selbstachtung in der Lage sein sollten, sie auf einem grundlegenden Niveau zu verwenden.
Schlüsselkonzepte beim Unit-Testen
Die Testabdeckung (Codeabdeckung) ist eines der Hauptmaßstäbe dafür, wie gut eine Anwendung getestet wird. Dies ist der Prozentsatz des Codes, der von den Tests abgedeckt wird (0–100 %). In der Praxis verfolgen viele diesen Prozentsatz als Ziel. Damit bin ich nicht einverstanden, denn es bedeutet, dass Tests dort eingesetzt werden, wo sie nicht benötigt werden. Angenommen, wir haben in unserem Dienst Standard-CRUD-Operationen (Erstellen/Abrufen/Aktualisieren/Löschen) ohne zusätzliche Logik. Diese Methoden sind reine Vermittler, die Arbeit an die Ebene delegieren, die mit dem Repository arbeitet. In dieser Situation müssen wir nichts testen, außer vielleicht, ob die angegebene Methode eine DAO-Methode aufruft, aber das ist ein Witz. Zur Bewertung der Testabdeckung werden normalerweise zusätzliche Tools verwendet: JaCoCo, Cobertura, Clover, Emma usw. Für eine detailliertere Untersuchung dieses Themas, TDD steht für testgetriebene Entwicklung. Bei diesem Ansatz schreiben Sie, bevor Sie etwas anderes tun, einen Test, der bestimmten Code überprüft. Dabei handelt es sich um einen Black-Box-Test: Wir kennen die Eingabe und wissen, wie die Ausgabe aussehen soll. Dadurch ist es möglich, Codeduplizierungen zu vermeiden. Testgetriebene Entwicklung beginnt mit dem Entwurf und der Entwicklung von Tests für jede einzelne Funktionalität Ihrer Anwendung. Beim TDD-Ansatz erstellen wir zunächst einen Test, der das Verhalten des Codes definiert und testet. Das Hauptziel von TDD besteht darin, Ihren Code verständlicher, einfacher und fehlerfreier zu machen.
- Wir schreiben unseren Test.
- Wir führen den Test durch. Es überrascht nicht, dass dies fehlschlägt, da wir die erforderliche Logik noch nicht implementiert haben.
- Fügen Sie den Code hinzu, der dafür sorgt, dass der Test bestanden wird (wir führen den Test erneut aus).
- Wir überarbeiten den Code.
Testphasen
Ein Test besteht aus drei Phasen:- Geben Sie Testdaten (Vorrichtungen) an.
- Üben Sie den zu testenden Code aus (rufen Sie die getestete Methode auf).
- Überprüfen Sie die Ergebnisse und vergleichen Sie sie mit den erwarteten Ergebnissen.

Testumgebungen
So, nun zur Sache. Für Java stehen mehrere Testumgebungen (Frameworks) zur Verfügung. Die beliebtesten davon sind JUnit und TestNG. Für unsere Rezension hier verwenden wir:
- @Test gibt an, dass es sich bei der Methode um einen Test handelt (im Grunde ist eine mit dieser Annotation gekennzeichnete Methode ein Komponententest).
- @Before bezeichnet eine Methode, die vor jedem Test ausgeführt wird. Zum Beispiel, um eine Klasse mit Testdaten zu füllen, Eingabedaten zu lesen usw.
- @After wird verwendet, um eine Methode zu markieren, die nach jedem Test aufgerufen wird (z. B. um Daten zu löschen oder Standardwerte wiederherzustellen).
- @BeforeClass wird analog zu @Before über einer Methode platziert. Eine solche Methode wird jedoch nur einmal vor allen Tests für die angegebene Klasse aufgerufen und muss daher statisch sein. Es wird verwendet, um ressourcenintensivere Vorgänge auszuführen, beispielsweise das Hochfahren einer Testdatenbank.
- @AfterClass ist das Gegenteil von @BeforeClass: Es wird einmal für die angegebene Klasse ausgeführt, jedoch erst nach allen Tests. Es wird beispielsweise verwendet, um persistente Ressourcen zu löschen oder die Verbindung zu einer Datenbank zu trennen.
- @Ignore bedeutet, dass eine Methode deaktiviert ist und während des gesamten Testlaufs ignoriert wird. Dies wird in verschiedenen Situationen verwendet, beispielsweise wenn die Basismethode geändert wurde und der Test noch nicht überarbeitet wurde, um die Änderungen zu berücksichtigen. In solchen Fällen ist es auch wünschenswert, eine Beschreibung hinzuzufügen, z. B. @Ignore("Some description").
- @Test(expected = Exception.class) wird für negative Tests verwendet. Hierbei handelt es sich um Tests, die überprüfen, wie sich die Methode im Fehlerfall verhält. Das heißt, der Test erwartet, dass die Methode eine Ausnahme auslöst. Eine solche Methode wird durch die Annotation @Test angezeigt, jedoch mit einem Hinweis darauf, welcher Fehler abgefangen werden soll.
- @Test(timeout = 100) prüft, ob die Methode in nicht mehr als 100 Millisekunden ausgeführt wird.
- @Mock wird über einem Feld verwendet, um ein Scheinobjekt zuzuweisen (dies ist keine JUnit-Annotation, sondern stammt von Mockito). Bei Bedarf legen wir das Verhalten des Mocks für eine bestimmte Situation direkt in der Testmethode fest.
- @RunWith(MockitoJUnitRunner.class) wird über einer Klasse platziert. Diese Annotation weist JUnit an, die Tests in der Klasse aufzurufen. Es gibt verschiedene Läufer, darunter diese: MockitoJUnitRunner, JUnitPlatform und SpringRunner. In JUnit 5 wurde die Annotation @RunWith durch die leistungsfähigere Annotation @ExtendWith ersetzt.
- AssertEquals(Objekt erwartet, Objekt tatsächlich) – prüft, ob die übergebenen Objekte gleich sind.
- affirmTrue(boolean flag) – prüft, ob der übergebene Wert wahr ist.
- affirmFalse(boolean flag) – prüft, ob der übergebene Wert falsch ist.
- affirmNull(Object object) – prüft, ob das übergebene Objekt null ist.
- affirmSame(Object firstObject, Object secondObject) – prüft, ob sich die übergebenen Werte auf dasselbe Objekt beziehen.
- affirmThat(T t, Matcher
Matcher) — Überprüft, ob t die im Matcher angegebene Bedingung erfüllt.
Testen in der Praxis
Schauen wir uns nun das obige Material in einem konkreten Beispiel an. Wir testen die Update-Methode eines Dienstes. Wir werden die DAO-Ebene nicht berücksichtigen, da wir die Standardebene verwenden. Fügen wir einen Starter für die Tests hinzu:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
Und hier haben wir die Serviceklasse:
@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
private final RobotDAO robotDAO;
@Override
public Robot update(Long id, Robot robot) {
Robot found = robotDAO.findById(id);
return robotDAO.update(Robot.builder()
.id(id)
.name(robot.getName() != null ? robot.getName() : found.getName())
.cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
.producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
.build());
}
}
Zeile 8 – Ziehen Sie das aktualisierte Objekt aus der Datenbank. Zeilen 9–14 – Erstellen Sie ein Objekt über den Builder. Wenn das eingehende Objekt ein Feld hat, legen Sie es fest. Wenn nicht, belassen wir den Inhalt in der Datenbank. Schauen Sie sich jetzt unseren Test an:
@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
@Mock
private RobotDAO robotDAO;
private RobotServiceImpl robotService;
private static Robot testRobot;
@BeforeClass
public static void prepareTestData() {
testRobot = Robot
.builder()
.id(123L)
.name("testRobotMolly")
.cpu("Intel Core i7-9700K")
.producer("China")
.build();
}
@Before
public void init() {
robotService = new RobotServiceImpl(robotDAO);
}
Zeile 1 – unser Läufer. Zeile 4 – wir isolieren den Dienst von der DAO-Schicht, indem wir ihn durch einen Schein ersetzen. Zeile 11 – wir legen eine Testentität (diejenige, die wir als Versuchskaninchen verwenden werden) für die Klasse fest. Zeile 22 – wir legen das Serviceobjekt fest, das wir testen werden.
@Test
public void updateTest() {
when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
Robot robotForUpdate = Robot
.builder()
.name("Vally")
.cpu("AMD Ryzen 7 2700X")
.build();
Robot resultRobot = robotService.update(123L, robotForUpdate);
assertNotNull(resultRobot);
assertSame(resultRobot.getId(),testRobot.getId());
assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
Hier sehen wir, dass der Test drei klare Unterteilungen hat: Zeilen 3-9 – Festlegung von Vorrichtungen. Zeile 11 – Ausführen des zu testenden Codes. Zeilen 13–17 – Überprüfung der Ergebnisse. Im Detail: Zeilen 3–4 – Legen Sie das Verhalten für den DAO-Mock fest. Zeile 5 – legen Sie die Instanz fest, die wir zusätzlich zu unserem Standard aktualisieren werden. Zeile 11 – verwenden Sie die Methode und nehmen Sie die resultierende Instanz. Zeile 13 – Überprüfen Sie, ob es nicht null ist. Zeile 14 – Vergleichen Sie die ID des Ergebnisses und die angegebenen Methodenargumente. Zeile 15 – Überprüfen Sie, ob der Name aktualisiert wurde. Zeile 16 – Sehen Sie sich das CPU-Ergebnis an. Zeile 17 – wir haben dieses Feld in der Instanz nicht angegeben, daher sollte es gleich bleiben. Diesen Zustand überprüfen wir hier. Lassen Sie es uns ausführen:
GO TO FULL VERSION