CodeGym /Java-Blog /Random-DE /Alles über Unit-Tests: Techniken, Konzepte, Praxis
John Squirrels
Level 41
San Francisco

Alles über Unit-Tests: Techniken, Konzepte, Praxis

Veröffentlicht in der Gruppe Random-DE
Heutzutage gibt es keine Anwendung, die nicht mit Tests überhäuft ist, daher ist dieses Thema für unerfahrene Entwickler relevanter denn je: Ohne Tests können Sie nicht erfolgreich sein. Lassen Sie uns überlegen, welche Arten von Tests grundsätzlich verwendet werden, und dann werden wir im Detail alles studieren, was Sie über Unit-Tests wissen müssen. Alles über Unit-Tests: Techniken, Konzepte, Praxis - 1

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.
Das klingt alles gut, aber wie funktioniert es in der Praxis? Einfach! Wir verwenden die Testpyramide von Mike Cohn: Alles über Unit-Tests: Techniken, Konzepte, Praxis - 2Dies ist eine vereinfachte Version der Pyramide: Sie ist jetzt in noch kleinere Teile unterteilt. Aber heute werden wir nicht zu anspruchsvoll werden. Wir betrachten die einfachste Version.
  1. 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.

  2. 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.

  3. 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. Alles über Unit-Tests: Techniken, Konzepte, Praxis - 3Der Ansatz besteht aus Folgendem:
  • 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.
TDD basiert auf Unit-Tests, da diese die kleinsten Bausteine ​​in der Testautomatisierungspyramide sind. Mit Unit-Tests können wir die Geschäftslogik jeder Klasse testen. BDD steht für verhaltensgesteuerte Entwicklung. Dieser Ansatz basiert auf TDD. Genauer gesagt werden einfache Sprachbeispiele verwendet, die das Systemverhalten für alle an der Entwicklung Beteiligten erklären. Auf diesen Begriff gehen wir nicht näher ein, da er vor allem Tester und Business-Analysten betrifft. Ein Testfall ist ein Szenario, das die Schritte, spezifischen Bedingungen und Parameter beschreibt, die zum Überprüfen des zu testenden Codes erforderlich sind. Bei einer Testvorrichtung handelt es sich um Code, der die Testumgebung so einrichtet, dass sie den Status aufweist, der für die erfolgreiche Ausführung der zu testenden Methode erforderlich ist. Dabei handelt es sich um eine vordefinierte Menge von Objekten und deren Verhalten unter bestimmten Bedingungen.

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.
Alles über Unit-Tests: Techniken, Konzepte, Praxis - 4Um die Modularität des Tests sicherzustellen, müssen Sie ihn von anderen Schichten der Anwendung isolieren. Dies kann mithilfe von Stubs, Mocks und Spys erfolgen. Mocks sind Objekte, die angepasst werden können (z. B. maßgeschneidert für jeden Test). Sie ermöglichen es uns, anzugeben, was wir von Methodenaufrufen erwarten, also die erwarteten Antworten. Wir verwenden Scheinobjekte, um zu überprüfen, ob wir das bekommen, was wir erwarten. Stubs bieten während des Tests eine fest codierte Antwort auf Aufrufe. Sie können auch Informationen über den Anruf speichern (zum Beispiel Parameter oder die Anzahl der Anrufe). Diese werden manchmal als Spione bezeichnet. Manchmal verwechseln die Leute die Begriffe „Stub“ und „Mock“: Der Unterschied besteht darin, dass ein Stub nichts prüft – er simuliert nur einen bestimmten Zustand. Ein Mock ist ein Objekt, das Erwartungen hat. Beispielsweise muss eine bestimmte Methode eine bestimmte Anzahl von Malen aufgerufen werden. Mit anderen Worten,

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: Alles über Unit-Tests: Techniken, Konzepte, Praxis – 5Ein JUnit-Test ist eine Methode in einer Klasse, die nur zum Testen verwendet wird. Die Klasse wird normalerweise genauso benannt wie die Klasse, die sie testet, mit dem Zusatz „Test“ am Ende. Zum Beispiel CarService -> CarServiceTest. Das Maven-Build-System schließt solche Klassen automatisch in den Testumfang ein. Tatsächlich wird diese Klasse als Testklasse bezeichnet. Lassen Sie uns kurz auf die grundlegenden Anmerkungen eingehen:

  • @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.
Werfen wir einen Blick auf einige Methoden zum Vergleichen von Ergebnissen:

  • 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.
AssertJ bietet auch eine nützliche Vergleichsmethode: AssertThat(firstObject).isEqualTo(secondObject) . Hier habe ich die grundlegenden Methoden erwähnt – die anderen sind Variationen der oben genannten.

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:Alles über Unit-Tests: Techniken, Konzepte, Praxis - 6Der Test ist grün! Wir können aufatmen :) Zusammenfassend lässt sich sagen, dass das Testen die Qualität des Codes verbessert und den Entwicklungsprozess flexibler und zuverlässiger macht. Stellen Sie sich vor, wie viel Aufwand es erfordert, Software mit Hunderten von Klassendateien neu zu entwerfen. Wenn wir für alle diese Klassen Komponententests geschrieben haben, können wir mit Zuversicht umgestalten. Und was am wichtigsten ist: Es hilft uns, Fehler während der Entwicklung leichter zu finden. Jungs und Mädels, das ist alles, was ich heute habe. Gib mir ein Like und hinterlasse einen Kommentar :)
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION