In diesem Beitrag wird eine umfangreiche Teststrategie im Rahmen der Softwareentwicklung vorgestellt, die funktionelle und nicht funktionelle Abdeckung durch automatisierte Testroutinen anstrebt. Auf die Bedeutung einer umfassenden Testabdeckung einer Anwendung wird an dieser Stelle nicht explizit eingegangen, sondern nur darauf verwiesen, dass funktionale und nicht-funktionale Tests unabdingbar werden, wenn eine Anwendung einen gewissen Komplexitätsgrad erreicht hat. Tests bilden einen bedeutenden Grundpfeiler in Entwicklung und Weiterentwicklung von moderner Software und in der Datenverarbeitung.[1,2]
Um von einem Anwendungsszenario auszugehen, das als Orientierungshilfe für das zu testende „Objekt“ dient, nehmen wir einen Anwendungsaufbau wie er in Abb. 1. schematisch gezeigt ist.
Die Anwendung hat eine oder mehrere Datenquellen und die Verarbeitung erfolgt nach dem gängigen Staging-Muster: Extrahieren, (Vorbereiten), Verarbeitung (processing), Nachbearbeitung (postprocssing) und Export. Hier ist es ohne weiteres denkbar Schritte auszulassen oder weitere hinzuzunehmen.
Wir nehmen an, die Datenverarbeitung erfolgt auf einem verteilten Dateisystem (HDFS) mittels des Apache Spark-Frameworks. Der beschriebene Aufbau ist für das vorgestellte Testvorgehen nicht erforderlich, er soll lediglich als ein möglicher Anwendungszweck herangezogen werden. Das vorgestellte Vorgehen kann je nach Einsatzzweck adaptiert werden. Um eine möglichst breite Abdeckung von Fehlerquellen zu gewährleisten, sollten Tests auf einer breiten Granularität der Anwendung ansetzen und dadurch unterschiedliche Aspekte und Fehlerquellen prüfen (vgl. Abb. 1). Dies kann mit Unit‑, Integration- und End-To-End-Tests erreicht werden. Im Folgenden werden der Einsatzzweck und Möglichkeiten einzelner Testarten ausgeführt.
Unit-Tests
Im Folgenden soll die bei Wikipedia angegebene Definition eines Unit-Test als Basis für ein gemeinsames Verständnis herangezogen werden:
“Unit testing is a software testing method by which individual units of source code—sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures—are tested to determine whether they are fit for use.”[3]
Mit dieser Art von Tests werden einzelne Funktions- und Logikschritte innerhalb einer Transformation (siehe Abb. 1) abgedeckt, wobei die gesamte Transformation an sich gegebenenfalls durch einen Unit-Test geprüft werden kann. Für die gängigen Programmiersprachen stehen Test-Frameworks zur Verfügung, welche die Konstruktion von Tests erheblich vereinfachen. Unit-Tests lassen sich darüber hinaus sehr gut für die Entwicklung einsetzen. Nach der Festlegung der Funktionalität kann zunächst ein Unit-Test erstellt und anschließend der dazugehörende Code geschrieben, beziehungsweise fertig gestellt werden Ein großer Vorteil der Unit-Tests besteht darin, dass mit den Tests stets der Entwicklungsstand überprüft werden kann. Wird hingegen der Test zuerst verfasst, muss sich der Entwickler mit dem Wesen der Funktionalität befassen, was häufig zu einem besseren Verständnis des Problems führt und somit in einem qualitativ hochwertigeren Code resultiert. Neben Black-Box Tests sollten White-Box Tests spezifische Eigenarten der Implementation berücksichtigen und auf Grenzfälle eingehen. Dies lässt sich gut mit randomisierten Tests verknüpfen, die zwar mehr Implementierungsaufwand bedeuten, jedoch nicht bedachte Fälle abdecken können.[1] Unit ‑Tests lassen sich in der Regel einfach umsetzen, sollten schnell und einfach auszuführen sein und bei jedem Build der Anwendung erfolgen, – nämlich als Prüfkriterium für einen erfolgreichen Build.
Wird mit dem Spark-Framework gearbeitet, müssen für die Tests Spark-Context und Spark-Session aufgebaut werden, dazu sollte in der Regel eine lokale Instanz genutzt werden. Der Aufbau von Spark-Context und Spark-Session nimmt mehr Zeit in Anspruch und verlangsamt deshalb die Testausführung, vor allem wenn dies in zahlreichen Tests der Fall ist. Abhilfe kann es sein Tests mit nur ein Spark-Context aufzubauen und daraus alle benötigten Spark-Sessions abzuleiten. Weiterhin kann es für komplexe Logik praktikabel sein auf die von Spark bereit gestellte map oder mapPartitions Methode zurückzugreifen. Innerhalb der Map-Funktion kann die Funktionalität der nativen Programmiersprache genutzt werden und somit auch die zur Verfügung stehenden Testmöglichkeiten. So kann eine komplexe Verarbeitung gut mit Unit-Tests abgedeckt werden, ohne dabei mit Datasets und DataFrames hantieren zu müssen.
Nichtsdestotrotz geben Unit-Tests so gut wie keine Auskunft zur Funktionalität der Verkettung von Codeabschnitten, die einzeln mit Unit-Tests abgedeckt sind. Das bedeutet ein erfolgreichen isolierter Test von Transformer 1 und Transformer 2 lässt nicht darauf schließen, dass Transformer 1 und 2 gemeinsam erfolgreich ausgeführt werden. Um dies sicherzustellen, sollte mit Hilfe von Integrationstests auf eine gröbere Granularität hin geprüft werden.
Integrations-Test
Unit tests pass, no integration tests pic.twitter.com/8geAsHgSBY
— Ryan Stortz (@withzombies) February 9, 2017
Für ein gemeinsamen Verständnis eines Integrationtests wird im Folgenden auf die in Wikipedia gegebene Definition zurückgegriffen:
“Integration testing (sometimes called integration and testing, abbreviated I&T) is the phase in software testing in which individual software modules are combined and tested as a group. Integration testing is conducted to evaluate the compliance of a system or component with specified functional requirements.” [4]
An dieser Stelle wird ein ganzes Modul mit definiertem Eingangs- und Ausgangsdatenstrom getestet (vgl. Abb. 1), damit wird das Zusammenspiel der Transformationen, die mit Unit-Tests geprüft werden, abgedeckt. Außerdem können Integrationstests vom Entwickler zur Verifizierung der Verarbeitungslogik genutzt werden, wenn die Integrationstests ohne ein Deployment-Schritt lokal ausgeführt werden können. Dies erspart zudem unnötige “Korrektur-Deployments”. Für die Erstellung eines Integrationstests müssen Ein- und Ausgabe nachgestellt/imitiert (Mock) werden, wofür gegebenenfalls ein von der Anwendung abweichendes Format gewählt werden kann. Somit lässt sich beispielsweise ein manuellen Abgleich einfacher ausführen. Der einfache Abgleich und manuelle Kontrolle bieten durchaus Vorteile bei der Entwicklung. Dabei ist anzustreben, die Tests nicht zu umfangreich zu gestalten, um eine Laufzeit im Minutenbereich zu ermöglichen.
Um das Zusammenspiel von Modulen zu prüfen, sollte ein weiteres Testszenario erfolgen, das die gesamte Anwendung testet.
End-To-End-Test
Unter einem End-to-End-Test versteht man den Durchlauf der gesamten Anwendung mit Berücksichtigung aller beteiligten technischen Komponenten inklusive des Imports und des Exports. Ein End-To-End Test erfordert einen höheren Aufwand als die anderen vorgestellten Testarten, stellt aber die technische Funktionalität der gesamten Anwendung sicher. Zusammen mit einer vordefinierten Eingabe und Prüfung der erwarteten Ausgabe (End-to-End Test als Regressions-Test) [5] kann zudem die fachliche Verifikation erfolgen. Dabei können mehrere Durchläufe vorgenommen werden, um unterschiedliche fachlichen Szenarien durchzuspielen. Die Verarbeitung großer Datenmengen kann ggf. mehrere Stunden in Anspruch nehmen. Je nach Zeitaufwand bietet es sich an hierfür nächtliche oder Wochenendläufe zu nutzen.
Zusammenfassung
Erst die Prüfungen und Tests auf unterschiedlichen Granularitätsstufen der Anwendung können eine zuverlässige Aussage über den Zustand einer Anwendung treffen. Eine automatisierte Prüfung unterschiedlicher Granularitätsstufen, bei der fachliche und technische Aspekte berücksichtigt werden, können die Entwicklung und Deployment beschleunigen, indem rasches Feedback durch die Testroutinen gegeben wird. Im Allgemeinen ist die Entwicklungsarbeit mit Tests ein iterativer Prozess und mit einem entsprechenden Aufwand verknüpft, der sich jedoch bei komplexen Anwendungen rasch bezahlt macht.
Verweise
[1] https://software.rajivprab.com/2019/04/28/rethinking-software-testing-perspectives-from-the-world-of-hardware/ (Stand 24.01.2021)
[2] https://en.wikipedia.org/wiki/Test-driven_development (Stand 24.01.2021)
[3] https://en.wikipedia.org/wiki/Unit_test (Stand 24.01.2021)
[4] https://en.wikipedia.org/wiki/Integration_testing (Stand 24.01.2021)
[5] https://en.wikipedia.org/wiki/Regression_testing (Stand 24.01.2021)