Überlegungen zu effektiven Unternehmenstests

Hallo Habr!

Vor kurzem sind wir zu einer gründlichen Untersuchung des Testthemas zurückgekehrt, und in den absehbaren Plänen haben wir sogar ein ausgezeichnetes Buch über Unit-Tests. Gleichzeitig glauben wir, dass der Kontext in diesem Thema wie nirgendwo anders wichtig ist. Deshalb bieten wir heute eine Übersetzung von zwei Veröffentlichungen (zu einer zusammengefasst) an, die auf dem Blog eines bekannten Java EE- Spezialisten Sebastian Dashner veröffentlicht wurden - nämlich 1/6 und 2/6 aus der Reihe „ Gedanken zu effizienten Unternehmenstests. "

Unternehmenstests sind ein Thema, das noch nicht so ausführlich untersucht wurde, wie wir es gerne hätten. Das Schreiben und insbesondere das Unterstützen von Tests erfordert viel Zeit und Mühe. Es ist jedoch keine Option, Zeit zu sparen, indem die Tests abgebrochen werden. Welche Aufgaben, Ansätze und Testtechnologien sollten untersucht werden, um die Effektivität des Testens zu erhöhen?

Einführung


Unabhängig von den verschiedenen Arten von Tests und ihrem Umfang besteht der Zweck der Vorbereitung einer Reihe von Tests darin, auf diesem Material sicherzustellen, dass unsere Anwendung in der Produktion genau wie erwartet funktioniert. Diese Motivation sollte die Hauptmotivation sein, wenn geprüft wird, ob das System die Aufgabe erfüllt, wenn wir dieses System aus Sicht des Benutzers betrachten.

Da Aufmerksamkeitsspanne und Kontextwechsel zu berücksichtigen sind, müssen wir sicherstellen, dass unsere Tests nicht lange ausgeführt und getestet werden und dass die Testergebnisse vorhersehbar sind. Beim Schreiben von Code ist eine schnelle Überprüfung des Codes (innerhalb einer Sekunde möglich) von entscheidender Bedeutung - dies gewährleistet eine hohe Produktivität und Konzentration während der Arbeit.
Auf der anderen Seite müssen wir die Testunterstützung sicherstellen. Softwareänderungen treten sehr häufig auf, und bei einer umfassenden Abdeckung des Codes mit Funktionstests erfordert jede Funktionsänderung im Produktionscode eine Änderung auf Testebene. Im Idealfall sollte sich der Testcode nur ändern, wenn sich die Funktionalität, d. H. Die Geschäftslogik, ändert, und nicht, wenn unnötiger Code bereinigt und umgestaltet wird. Im Allgemeinen sollten Testszenarien die Möglichkeit nichtfunktionaler struktureller Änderungen beinhalten.

Wenn wir die verschiedenen Anwendungsbereiche von Tests betrachten, stellt sich die Frage: Welche dieser Bereiche sind die Zeit und Mühe wert? Beispielsweise sind in Mikroservice-Anwendungen sowie in jedem System, das erhebliche Arbeit an der Verteilung und Integration von Code leistet, Integrationstests besonders wichtig, um die Grenzen des Systems zu erfassen. Daher benötigen wir eine effektive Methode, um die gesamte Anwendung während der lokalen Entwicklung zu testen und gleichzeitig die Umgebung und Struktur dieser Anwendung in einer Form zu erhalten, die so produktionsnah wie möglich ist.

Prinzipien und Einschränkungen


Unabhängig von den ausgewählten Lösungen definieren wir die folgenden Prinzipien und Einschränkungen für unsere Testsuite:

  • , . , . , , .
  • , , . , , , . , , .
  • - . , , .
  • , , . : « HTTP- gRPC, JSON - enterprise-, ..?”.
  • , , -. API, DSL .
  • « », , , , , , “dev” debug () , dev Quarkus', Telepresence, watch-and-deploy (« ») .
  • . , , , , . .
  • , , -, , , . , , , .


Der Komponententest überprüft das Verhalten eines einzelnen Moduls, normalerweise einer Klasse, während alle externen Faktoren, die nicht mit der Struktur des Moduls zusammenhängen, ignoriert oder simuliert werden. Unit-Tests sollten die Geschäftslogik einzelner Module überprüfen, ohne deren weitere Integration oder Konfiguration zu überprüfen.

Nach meiner Erfahrung haben die meisten Unternehmensentwickler eine ziemlich gute Vorstellung davon, wie Komponententests kompiliert werden. Um einen Eindruck davon zu hinterlassen, können Sie dieses Beispiel in meinem Kaffeetestprojekt sehen .. In den meisten Projekten wird JUnit in Kombination mit Mockito verwendet, um Abhängigkeiten zu simulieren, und idealerweise mit AssertJ, um lesbare Anweisungen effizient zu definieren. Ich betone immer, dass Unit-Tests ohne spezielle Erweiterungen oder Starter durchgeführt werden können, dh mit der üblichen JUnit. Die Erklärung ist einfach: Es geht nur um die Laufzeit, da wir die Fähigkeit benötigen, Hunderte von Tests in Millisekunden auszuführen.

Unit-Tests laufen in der Regel sehr schnell und es ist einfach, komplexe Testsuiten oder spezielle Workflows daraus zusammenzustellen, da sie einfach auszuführen sind und den Lebenszyklus des Tests nicht einschränken.

Wenn Sie jedoch viele Komponententests haben, die die Abhängigkeiten der getesteten Klasse simulieren, gibt es einen Nachteil: Sie hängen eng mit der Implementierung zusammen (dies gilt insbesondere für Klassen und Methoden), weshalb unser Code schwer umzugestalten ist. Mit anderen Worten, für jeden Refactoring-Vorgang im Produktionscode sind auch Änderungen am Testcode erforderlich. Im schlimmsten Fall lehnen Entwickler das Refactoring sogar teilweise ab, einfach weil es zu aufwändig ist und die Qualität des Codes im Projekt rapide abnimmt. Im Idealfall sollte der Entwickler in der Lage sein, die Elemente umzugestalten und neu anzuordnen, vorausgesetzt, es gibt keine Änderungen in der Anwendung, die für den Benutzer erkennbar sind. Unit-Tests vereinfachen keinesfalls immer die Umgestaltung des Produktionscodes.

Die Erfahrung zeigt jedoch, dass Komponententests sehr effektiv bei der Überprüfung von Code sind, der dicht mit präziser Logik gefüllt ist oder die Implementierung einer bestimmten Funktion, beispielsweise eines Algorithmus, beschreibt und gleichzeitig nicht sehr aktiv mit anderen Komponenten interagiert. Je weniger komplex oder dicht der Code in einer bestimmten Klasse ist, je geringer seine zyklomatische Komplexität ist oder je aktiver er mit anderen Komponenten interagiert, desto weniger effektiv sind Unit-Tests beim Testen dieser Klasse. Insbesondere in Fällen mit Microservices, in denen relativ wenig Geschäftslogik enthalten ist, aber eine umfassende Integration mit externen Systemen bereitgestellt wird, besteht möglicherweise in vielen Fällen keine Notwendigkeit, Komponententests zu verwenden. In solchen Systemen enthalten einzelne Module (mit seltenen Ausnahmen) normalerweise wenig spezialisierte Logik. Dies sollte bei der Entscheidung berücksichtigt werdenWas ist angemessener, um Zeit und Mühe zu verbringen.

Anwendungssituationen testen


Um das Problem der engen Verknüpfung von Tests mit der Implementierung zu lösen, können Sie einen etwas anderen Ansatz ausprobieren, um den Umfang der Tests zu erweitern. In meinem Buch schrieb ich über Komponententests, weil ich keinen besseren Begriff finden konnte; Im Wesentlichen geht es in diesem Fall jedoch darum, angewandte Situationen zu testen.

Anwendungssituationstests sind Integrationstests, die auf Codeebene ausgeführt werden und keine integrierten Container verwenden. Sie werden abgebrochen, um den Start zu beschleunigen. Sie testen die Geschäftslogik gut koordinierter Komponenten, die normalerweise in einem bestimmten praktischen Fall verwendet werden, von der Grenzmethode bis hin zu allen ihnen zugeordneten Komponenten. Die Integration in externe Systeme, beispielsweise in Datenbanken, wird mithilfe von Mocks nachgeahmt.

Das Erstellen solcher Szenarien ohne den Einsatz fortschrittlicherer Technologien, die Komponenten automatisch verbinden, scheint eine große Arbeit zu sein. Wir definieren jedoch wiederverwendbare Testkomponenten. Sie sind auch Testgegenstücke, die Komponenten durch Simulieren, Verbinden und Hinzufügen von Testkonfigurationen erweitern. All dies geschieht, um den Gesamtaufwand für das Refactoring zu minimieren. Ziel ist es, die einzigen Verantwortlichkeiten zu schaffen, die den Einfluss von Änderungen auf eine einzelne Klasse (oder mehrere Klassen) im Testbereich begrenzen. Wenn wir solche Arbeiten durchführen, um sie wiederzuverwenden, reduzieren wir den Gesamtaufwand für die erforderliche Arbeit. Eine solche Strategie ist gerechtfertigt, wenn das Projekt wächst. Jede Komponente erfordert jedoch nur geringfügige Reparaturen, und diese Arbeiten werden schnell amortisiert.

Nehmen wir an, wir testen eine Klasse, die die Reihenfolge des Kaffees beschreibt, um sich das alles besser vorstellen zu können. Diese Klasse enthält zwei weitere Klassen: CoffeeShopund OrderProcessor.



Klassen von Test verdoppelt, CoffeeShopTestDoubleund OrderProcessorTestDoublesie *TDwerden im Testbereich des Projekts befinden, wo sie die Komponenten erben CoffeeShopund OrderProcessorbefindet sich im Hauptbereich des Programms. Testgegenstücke können die erforderliche Simulations- und Verbindungslogik festlegen und möglicherweise die öffentliche Schnittstelle der Klasse mithilfe der in dieser Anwendung benötigten Simulationsmethoden oder durch Überprüfungsmethoden erweitern.

Das Folgende zeigt die Testdoppelklasse für die Komponente CoffeeShop:

public class CoffeeShopTestDouble extends CoffeeShop {

    public CoffeeShopTestDouble(OrderProcessorTestDouble orderProcessorTestDouble) {
        entityManager = mock(EntityManager.class);
        orderProcessor = orderProcessorTestDouble;
    }

    public void verifyCreateOrder(Order order) {
        verify(entityManager).merge(order);
    }

    public void verifyProcessUnfinishedOrders() {
        verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class);
    }

    public void answerForUnfinishedOrders(List<Order> orders) {
        //     
    }
}

Die Test-Doppelklasse kann auf die Felder und Konstruktoren der CoffeeShop-Basisklasse zugreifen, um Abhängigkeiten herzustellen. Hier werden in Form von Testzwillingen auch Varianten anderer Komponenten verwendet, insbesondere werden OrderProcessorTestDoublesie benötigt, um zusätzliche Simulations- oder Verifizierungsmethoden aufzurufen, die Teil des praktischen Falls sind.

Klassen von Testdoppeln sind wiederverwendbare Komponenten, von denen jede einmal pro Umfang jedes Projekts geschrieben wird und dann in vielen praktischen Fällen verwendet wird:

class CoffeeShopTest {

    private CoffeeShopTestDouble coffeeShop;
    private OrderProcessorTestDouble orderProcessor;

    @BeforeEach
    void setUp() {
        orderProcessor = new OrderProcessorTestDouble();
        coffeeShop = new CoffeeShopTestDouble(orderProcessor);
    }

    @Test
    void testCreateOrder() {
        Order order = new Order();
        coffeeShop.createOrder(order);
        coffeeShop.verifyCreateOrder(order);
    }

    @Test
    void testProcessUnfinishedOrders() {
        List<Order> orders = Arrays.asList(...);
        coffeeShop.answerForUnfinishedOrders(orders);

        coffeeShop.processUnfinishedOrders();

        coffeeShop.verifyProcessUnfinishedOrders();
        orderProcessor.verifyProcessOrders(orders);
    }

}

Der Komponententest überprüft in diesem Fall den spezifischen Fall der Geschäftslogik, die am Einstiegspunkt aufgerufen wird CoffeeShop. Solche Tests werden präzise und lesbar erhalten, da alle Verbindungen und Simulationen in separaten Testzwillingen durchgeführt werden und später hochspezialisierte Screening-Techniken verwendet werden können, wie z verifyProcessOrders().

Wie Sie sehen können, erweitert die Testklasse den Bereich der Produktionsklasse, sodass Sie mokee installieren und Methoden verwenden können, die das Verhalten überprüfen. Trotz der Tatsache, dass die Einrichtung eines solchen Systems anscheinend viel Aufwand erfordert, werden diese Kosten schnell amortisiert, wenn wir im Rahmen des gesamten Projekts viele praktische Fälle haben, in denen Komponenten wiederverwendet werden können. Je mehr unser Projekt wächst, desto nützlicher wird dieser Ansatz - insbesondere, wenn Sie auf die Zeit achten, die für die Durchführung der Tests benötigt wird. Alle unsere Testfälle werden weiterhin mit JUnit ausgeführt und in kürzester Zeit in Hunderten ausgeführt.

Dies ist der Hauptvorteil dieses Ansatzes: Komponententests werden genauso schnell ausgeführt wie reguläre Komponententests. Sie stimulieren jedoch das Refactoring des Produktionscodes, da Änderungen an einer einzelnen Komponente oder nur an wenigen Komponenten vorgenommen werden müssen. Durch die Verbesserung der Testgegenstücke mit ausdrucksstarken Optimierungs- und Überprüfungsmethoden, die für unser Fachgebiet spezifisch sind, verbessern wir außerdem die Lesbarkeit unseres Codes, erleichtern dessen Verwendung und beseitigen stereotypen Code in Testskripten.

All Articles