JavaRush /Java-Blog /Random-DE /Java Unit Testing: Techniken, Konzepte, Praxis

Java Unit Testing: Techniken, Konzepte, Praxis

Veröffentlicht in der Gruppe Random-DE
Heutzutage wird es kaum noch eine Anwendung geben, die nicht mit Tests abgedeckt ist, daher wird dieses Thema für unerfahrene Entwickler relevanter denn je sein: Ohne Tests kommt man nicht weiter. Als Werbung empfehle ich Ihnen, sich meine früheren Artikel anzusehen. Einige davon behandeln Tests (und trotzdem werden die Artikel sehr nützlich sein):
  1. Integrationstest einer Datenbank mit MariaDB als Ersatz für MySql
  2. Implementierung einer mehrsprachigen Anwendung
  3. Speichern von Dateien in der Anwendung und Daten darüber in der Datenbank
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.

Arten von Tests

Was ist ein Test? Wie Wiki sagt: „ Ein Test oder Versuch ist eine Möglichkeit, die zugrunde liegenden Prozesse eines Systems zu untersuchen, indem das System in verschiedene Situationen versetzt und beobachtbare Änderungen darin verfolgt werden.“ Mit anderen Worten handelt es sich hierbei um einen Test der korrekten Funktion unseres Systems in bestimmten Situationen. Alles über Unit-Tests: Methoden, Konzepte, Praxis - 2Schauen wir uns mal an, welche Arten von Tests es gibt:
  1. Unit-Tests sind Tests, deren Aufgabe es ist, jedes Modul des Systems einzeln zu testen. Es ist wünschenswert, dass es sich um minimal teilbare Teile des Systems handelt, beispielsweise um Module.

  2. Beim Systemtest handelt es sich um einen High-Level-Test zum Testen des Betriebs eines größeren Teils einer Anwendung oder des Systems als Ganzes.

  3. Unter Regressionstests versteht man Tests, mit denen überprüft wird, ob neue Funktionen oder Fehlerbehebungen die bestehende Funktionalität der Anwendung beeinträchtigen und ob alte Fehler erneut auftreten.

  4. Beim Funktionstest wird die Übereinstimmung eines Teils der Anwendung mit den in Spezifikationen, User Stories usw. genannten Anforderungen überprüft.

    Arten von Funktionstests:

    • „White-Box“-Test zur Übereinstimmung eines Teils der Anwendung mit den Anforderungen mit Kenntnis der internen Implementierung des Systems;
    • „Black-Box“-Test zur Übereinstimmung eines Teils der Anwendung mit den Anforderungen ohne Kenntnis der internen Implementierung des Systems.
  5. Leistungstests sind eine Art von Tests, die geschrieben werden, um die Geschwindigkeit zu bestimmen, mit der ein System oder ein Teil davon unter einer bestimmten Last läuft.
  6. Lasttests – Tests, die dazu dienen, die Stabilität des Systems unter Standardlasten zu überprüfen und den maximal möglichen Spitzenwert zu ermitteln, bei dem die Anwendung ordnungsgemäß funktioniert.
  7. Bei Stresstests handelt es sich um eine Testart, mit der die Funktionalität einer Anwendung unter nicht standardmäßigen Belastungen überprüft und der maximal mögliche Spitzenwert ermittelt werden soll, bei dem das System nicht abstürzt.
  8. Sicherheitstests – Tests zur Überprüfung der Sicherheit eines Systems (vor Angriffen durch Hacker, Viren, unbefugtem Zugriff auf vertrauliche Daten und anderen Lebensfreuden).
  9. Beim Lokalisierungstest handelt es sich um einen Lokalisierungstest für eine Anwendung.
  10. Bei Usability-Tests handelt es sich um eine Art von Tests, die darauf abzielen, die Benutzerfreundlichkeit, Verständlichkeit, Attraktivität und Erlernbarkeit für Benutzer zu überprüfen.
  11. Das klingt alles gut, aber wie funktioniert es in der Praxis? Es ist ganz einfach: Es wird die Testpyramide von Mike Cohn verwendet: Alles über Unit-Tests: Methoden, Konzepte, Praxis - 4Dies ist eine vereinfachte Version der Pyramide: Jetzt ist sie in kleinere Teile unterteilt. Aber heute werden wir nicht pervertieren und die einfachste Option in Betracht ziehen.
    1. Unit – Unit-Tests, die in verschiedenen Schichten der Anwendung verwendet werden und die kleinste teilbare Logik der Anwendung testen: zum Beispiel eine Klasse, meistens jedoch eine Methode. Diese Tests versuchen normalerweise, so viel wie möglich von der externen Logik zu isolieren, d. h. den Eindruck zu erwecken, dass der Rest der Anwendung im Standardmodus arbeitet.

      Von diesen Tests sollte es immer viele geben (mehr als bei anderen Typen), da sie kleine Teile testen, sehr leichtgewichtig sind und nicht viele Ressourcen verbrauchen (mit Ressourcen meine ich RAM und Zeit).

    2. Integration – Integrationstests. Es prüft größere Teile des Systems, das heißt entweder die Kombination mehrerer Logikteile (mehrere Methoden oder Klassen) oder die Korrektheit der Arbeit mit einer externen Komponente. Normalerweise gibt es weniger dieser Tests als Unit-Tests, da sie schwerer sind.

      Als Beispiel für Integrationstests können Sie in Betracht ziehen, eine Verbindung zu einer Datenbank herzustellen und zu überprüfen, ob die damit arbeitenden Methoden ordnungsgemäß funktionieren .

    3. UI – Tests, die die Funktion der Benutzeroberfläche überprüfen. Sie beeinflussen die Logik auf allen Ebenen der Anwendung, weshalb sie auch End-to-End genannt werden. Davon gibt es in der Regel deutlich weniger, da sie am schwersten sind und die notwendigsten (benutzten) Wege prüfen müssen.

      In der Abbildung oben sehen wir das Verhältnis der Flächen verschiedener Teile des Dreiecks: Bei der Anzahl dieser Tests in der realen Arbeit bleibt ungefähr das gleiche Verhältnis erhalten.

      Heute werfen wir einen genaueren Blick auf die am häufigsten verwendeten Tests – Unit-Tests, da alle Java-Entwickler mit etwas Selbstachtung in der Lage sein sollten, sie auf einem grundlegenden Niveau zu verwenden.

    Schlüsselkonzepte des Unit-Tests

    Die Testabdeckung (Code Coverage) ist eine der wichtigsten Beurteilungen der Qualität von Anwendungstests. Dies ist der Prozentsatz des Codes, der von Tests abgedeckt wurde (0–100 %). In der Praxis verfolgen viele Leute diesen Prozentsatz, mit dem ich nicht einverstanden bin, da sie anfangen, Tests dort hinzuzufügen, wo sie nicht benötigt werden. Unser Dienst verfügt beispielsweise über 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 haben wir nichts zu testen: Vielleicht ruft diese Methode eine Methode aus dem Tao auf, aber das ist nicht ernst. Zur Beurteilung der Testabdeckung werden in der Regel zusätzliche Tools verwendet: JaCoCo, Cobertura, Clover, Emma usw. Für eine detailliertere Untersuchung dieses Problems halten Sie einige geeignete Artikel bereit: TDD (Testgetriebene Entwicklung) – testgetriebene Entwicklung. Bei diesem Ansatz wird zunächst ein Test geschrieben, der einen bestimmten Code überprüft. Es stellt sich heraus, dass es sich um einen Black-Box-Test handelt: Wir wissen, was am Eingang ist, und wir wissen, was am Ausgang passieren soll. Dadurch wird Codeduplizierung vermieden. Die testgetriebene Entwicklung beginnt mit dem Entwurf und der Entwicklung von Tests für jede kleine Funktionalität der Anwendung. Beim TDD-Ansatz wird zunächst ein Test entwickelt, der definiert und überprüft, was der Code tun wird. Das Hauptziel von TDD besteht darin, Code klarer, einfacher und fehlerfrei zu machen. Alles über Unit-Tests: Methoden, Konzepte, Praxis - 6Der Ansatz besteht aus folgenden Komponenten:
    1. Wir schreiben unseren Test.
    2. Wir führen den Test durch, unabhängig davon, ob er bestanden wurde oder nicht (wir sehen, dass alles rot ist – keine Panik: So sollte es sein).
    3. Wir fügen den Code hinzu, der diesen Test erfüllen soll (führen Sie den Test aus).
    4. Wir überarbeiten den Code.
    Basierend auf der Tatsache, dass Unit-Tests die kleinsten Elemente in der Testautomatisierungspyramide sind, basiert TDD auf ihnen. Mit Hilfe von Unit-Tests können wir die Geschäftslogik jeder Klasse testen. BDD (Behavior-driven Development) – Entwicklung durch Verhalten. Dieser Ansatz basiert auf TDD. Genauer gesagt werden in klarer Sprache verfasste Beispiele (meist in Englisch) verwendet, die das Verhalten des Systems für alle an der Entwicklung Beteiligten veranschaulichen. Auf diesen Begriff gehen wir nicht tiefer ein, da er hauptsächlich Tester und Business-Analysten betrifft. Testfall – ein Skript, das die Schritte, spezifischen Bedingungen und Parameter beschreibt, die zur Überprüfung der Implementierung des zu testenden Codes erforderlich sind. Fixture ist ein Zustand der Testumgebung, der für die erfolgreiche Ausführung der zu testenden Methode erforderlich ist. Hierbei handelt es sich um eine vorgegebene Menge von Objekten und deren Verhalten unter den verwendeten Bedingungen.

    Testphasen

    Der Test besteht aus drei Stufen:
    1. Angabe der zu testenden Daten (Fixtures).
    2. Verwendung des zu testenden Codes (Aufruf der zu testenden Methode).
    3. Überprüfen Sie die Ergebnisse und vergleichen Sie sie mit den erwarteten.
    Alles über Unit-Tests: Methoden, Konzepte, Praxis - 7Um die Modularität des Tests sicherzustellen, müssen Sie von anderen Schichten der Anwendung isoliert sein. Dies kann mithilfe von Stubs, Mocks und Spys erfolgen. Mocks sind Objekte, die anpassbar sind (z. B. spezifisch für jeden Test) und es Ihnen ermöglichen, Erwartungen für Methodenaufrufe in Form von Antworten festzulegen, die wir erhalten möchten. Erwartungsprüfungen werden durch Aufrufe von Mock-Objekten durchgeführt. Stubs – bieten eine fest verdrahtete Antwort auf Anrufe während des Tests. Sie können auch Informationen über den Anruf speichern (z. B. Parameter oder die Anzahl dieser Anrufe). Diese werden manchmal mit ihrem eigenen Begriff „Spion“ ( Spion ) bezeichnet . Manchmal werden die Begriffe „Stub“ und „Mock“ verwechselt: Der Unterschied besteht darin, dass ein Stub nichts prüft, sondern nur einen bestimmten Zustand simuliert. Ein Mock ist ein Objekt, das Erwartungen hat. Beispielsweise muss eine bestimmte Klassenmethode eine bestimmte Anzahl von Malen aufgerufen werden. Mit anderen Worten: Ihr Test wird niemals aufgrund eines Stubs abbrechen, wohl aber aufgrund eines Mocks.

    Testumgebungen

    Kommen wir nun zur Sache. Für Java stehen mehrere Testumgebungen (Frameworks) zur Verfügung. Die beliebtesten davon sind JUnit und TestNG. Für unsere Überprüfung verwenden wir: Alles über Unit-Tests: Methoden, Konzepte, Praxis - 8Ein JUnit-Test ist eine in einer Klasse enthaltene Methode, die nur zum Testen verwendet wird. Eine Klasse wird normalerweise genauso benannt wie die Klasse, die sie testet, mit +Test am Ende. Zum Beispiel CarService→ CarServiceTest. Das Maven-Build-System fügt solche Klassen automatisch in den Testbereich ein. Tatsächlich wird diese Klasse als Testklasse bezeichnet. Lassen Sie uns die grundlegenden Anmerkungen ein wenig durchgehen: @Test – Definition dieser Methode als Testmethode (tatsächlich ist die mit dieser Anmerkung gekennzeichnete Methode ein Komponententest). @Before – markiert die Methode, die vor jedem Test ausgeführt wird. Zum Beispiel Füllen von Klassentestdaten, Lesen von Eingabedaten usw. @After – wird über der Methode platziert, die nach jedem Test aufgerufen wird (Daten bereinigen, Standardwerte wiederherstellen). @BeforeClass – über der Methode platziert – analog zu @Before. Diese Methode wird jedoch nur einmal vor allen Tests für eine bestimmte Klasse aufgerufen und muss daher statisch sein. Es wird verwendet, um anspruchsvollere Vorgänge durchzuführen, beispielsweise das Hochladen einer Testdatenbank. @AfterClass ist das Gegenteil von @BeforeClass: wird einmal für eine bestimmte Klasse ausgeführt, aber nach allen Tests ausgeführt. Wird beispielsweise verwendet, um persistente Ressourcen zu bereinigen oder die Verbindung zur Datenbank zu trennen. @Ignore – weist darauf hin, dass die folgende Methode deaktiviert ist und beim Ausführen von Tests insgesamt ignoriert wird. Es wird in verschiedenen Fällen verwendet, beispielsweise wenn die Basismethode geändert wurde und keine Zeit blieb, den Test dafür zu wiederholen. In solchen Fällen empfiehlt es sich auch, eine Beschreibung hinzuzufügen – @Ignore("Some description"). @Test (expected = Exception.class) – wird für negative Tests verwendet. Hierbei handelt es sich um Tests, die prüfen, wie sich eine 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 gekennzeichnet, weist jedoch einen abzufangenden Fehler auf. @Test(timeout=100) – prüft, ob die Methode in nicht mehr als 100 Millisekunden ausgeführt wird. @Mock – eine Klasse wird über einem Feld verwendet, um ein bestimmtes Objekt als Mock festzulegen (dies ist nicht aus der Junit-Bibliothek, sondern von Mockito), und wenn wir es brauchen, legen wir das Verhalten des Mocks in einer bestimmten Situation fest , direkt in der Testmethode. @RunWith(MockitoJUnitRunner.class) – die Methode wird über der Klasse platziert. Dies ist die Schaltfläche zum Ausführen von Tests. Läufer können unterschiedlich sein: Es gibt beispielsweise die folgenden: MockitoJUnitRunner, JUnitPlatform, SpringRunner usw.). 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(Object expecteds, Object actuals)— prüft, ob die übertragenen Objekte gleich sind.
    • assertTrue(boolean flag)— prüft, ob der übergebene Wert „true“ zurückgibt.
    • assertFalse(boolean flag)– prüft, ob der übergebene Wert „false“ zurückgibt.
    • assertNull(Object object)– prüft, ob das Objekt null ist.
    • assertSame(Object firstObject, Object secondObject)— prüft, ob sich die übergebenen Werte auf dasselbe Objekt beziehen.
    • assertThat(T t, Matcher<T> matcher)— prüft, ob t die im Matcher angegebene Bedingung erfüllt.
    Es gibt auch ein nützliches Vergleichsformular von Assertj – assertThat(firstObject).isEqualTo(secondObject) hier habe ich über die grundlegenden Methoden gesprochen, da der Rest verschiedene Variationen der oben genannten sind.

    Testpraxis

    Schauen wir uns nun das obige Material anhand eines konkreten Beispiels an. Wir werden die Methode für den Dienst testen – Update. Wir werden die Dao-Schicht nicht berücksichtigen, da sie unsere Standardeinstellung ist. Fügen wir einen Starter für Tests hinzu:
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <version>2.2.2.RELEASE</version>
       <scope>test</scope>
    </dependency>
    Also 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());
       }
    }
    8 - Ziehen Sie das aktualisierte Objekt aus der Datenbank. 9-14 - Erstellen Sie das Objekt über den Builder. Wenn das eingehende Objekt ein Feld hat, legen Sie es fest. Wenn nicht, belassen Sie das, was in der Datenbank ist. Schauen Sie sich 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);
       }
    1 – unser Runner 4 – isolieren Sie den Dienst von der Dao-Schicht durch Ersetzen eines Scheins. 11 – legen Sie eine Testentität für die Klasse fest (diejenige, die wir als Testhamster verwenden werden). 22 – legen Sie ein Dienstobjekt 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 eine klare Aufteilung des Tests in drei Teile: 3-9 – Fixtures einstellen 11 – Ausführen des getesteten Teils 13-17 – Überprüfen der Ergebnisse Weitere Details: 3-4 – Festlegen des Verhaltens für Moka Dao 5 – Festlegen einer Instanz dass wir zusätzlich zu unserem Standard aktualisieren werden 11 - Verwenden Sie die Methode und nehmen Sie die resultierende Instanz 13 - Überprüfen Sie, ob sie nicht Null ist 14 - Überprüfen Sie die Ergebnis-ID und die angegebenen Methodenargumente 15 - Überprüfen Sie, ob der Name aktualisiert wurde 16 - Schauen Sie beim Ergebnis von CPU 17 – da wir dies nicht im Feld „Update-Instanz“ eingestellt haben, sollte es gleich bleiben, überprüfen wir es. Alles über Unit-Tests: Methoden, Konzepte, Praxis - 9Lassen Sie uns starten: Alles über Unit-Tests: Techniken, Konzepte, Praxis – 10Der Test ist grün, Sie können ausatmen)) Fassen wir also zusammen: Tests verbessern die Qualität des Codes und machen den Entwicklungsprozess flexibler und zuverlässiger. Stellen Sie sich vor, wie viel Aufwand wir aufwenden müssten, wenn wir Software mit Hunderten von Klassendateien neu entwerfen würden. Sobald 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. Leute, das ist alles für mich heute: Likes abgeben, Kommentare schreiben))) Alles über Unit-Tests: Methoden, Konzepte, Praxis - 11
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION