Shiny-Apps mit testthat (unit-) testen

In diesem Blog-Beitrag wird erklärt, wie man eine Shiny-App mithilfe der Pakete shinytest und testthat testen kann. Grundlegende Vorkenntnisse über Shiny-Apps und das Prinzip von Unit-Tests mit dem Paket testthat sind nützlich, werden hier aber nicht vorausgesetzt.

Beispiel einer Shiny-App

Für den hier vorgestellten Test sind die Pakete shiny (aktuell: 1.1.0), testthat (1.3.0) und shinytest (2.0.0) erforderlich. Falls nötig können sie bequem mit install.packages() installiert werden.

Unten ist ein minimales Beispiel einer Shiny-App (app.R), die getestet werden soll. Die App hat nur einen einzigen numerischen Input und einen Text-Output. Die eingegebene Zahl n wird quadriert und das Ergebnis wird als Text angezeigt.

Und so sieht die App aus:

Was ist shinytest?

Das Paket shinytest ermöglicht das automatische Testen einer Shiny-App. Dabei wird sowohl das “Aussehen” der App, sowie ihr “interner” Zustand zu einem bestimmten Zeitpunkt im Programmablauf untersucht. Mithilfe eines interaktiven User-Interfaces lassen sich Snapshots (genauer gesagt Referenz-Snapshots) sowie eine Test-Datei erstellen. Die Test-Datei beinhaltet den Code, der für spätere Wiederherstellung der Snapshots erforderlich ist. Bei jedem weiteren Test werden neue Snapshots erstellt und mit den Referenz-Snapshots verglichen, um unerwartetes Verhalten der Shiny-App automatisch zu detektieren. Mehr zu dem normalen Workflow mit shinytest ist hier zu finden. In diesem Blog-Beitrag wird allerdings ein anderer Umgang mit dem Testen beschrieben; Nämlich das gemeinsame Testen mittels shinytest und testtthat [*].

[*] Nicht hier besprochen wird die Funktion expect_pass (vgl. ?expect_pass), die einfach alle shinytest-Snapshots vergleicht und über Abweichungen informiert. Das erlaubt zwar schnelle Vergleiche, aber ist weniger detailliert und spezifisch als der hier vorgestellte Ansatz.

Shiny-Apps mit shinytest & testthat testen

shinytest verfügt über die Klasse ShinyDriver (vgl. ?ShinyDriver), die beim Erstellen eines neuen Objekts die Shiny-App in einer neuen R-Session sowie eine Instanz von PhantomJS öffnet und diese automatisch mit der Shiny-App verbindet. PhantomJS ist ein Headless Webbrowser, der durch JavaScript bedient werden kann. Das ShinyDriver-Objekt ist mit verschiedenen Methoden ausgerüstet, die vor allem das Auslesen bzw. Eingeben von Werten für verschiedene Variablen in der shiny-App ermöglichen. Man kann also den Input-Variablen beliebige Werte gezielt, “manuell” (ohne das übliche User-Interface der Shiny-App) zuweisen und dann die Ausgabe-Variablen auslesen.

Beispiel eines Tests

In folgendem Test wird die Variable num_input auf 30 gesetzt und dann getestet, ob die Variable text_out zum String “Das Quadrat der Zahl n lautet: n² = 900” wird. Mehr zum Testen mit testthat ist hier zu finden.

Mit den Erwartungsfunktionen des Pakets testthat ist es also bequem möglich, die Funktionalitäten der Shiny-App zu testen. Ein Vorteil dabei ist auch, dass beim Aufrufen von devtools::test() sowohl die Tests der Shiny-App als auch die weiteren Unit-Tests mitberücksichtigt werden.

Tiefere Einsichten – Exportierte Variablen und HTML-Widgets

Innerhalb der server-Funktion von Shiny kann man zudem neue Variablen (außer der üblichen Inputs und Outputs) extra definieren. Diese sind dann von shinytest-Seite “sichtbar” und erlauben eine detailliertere Untersuchung der App-Abläufe. Als Beispiel für die oben gezeigte Shiny-App kann man die Liste aller eingegebenen Zahlen n speichern und als Variable inputs_list exportieren (bitte siehe den Code unten).

Für verschiedene HTML-Widgets bietet sich die Methode findElement mit der XPath zu verwenden app$findElement(xpath = "Hier das XPATH"). Wenn man beispielsweise Benachrichtigungen mit der Funktion showNotification() in der Shiny-App zeigt, kann man sie mit xpath = "//*[@id=\"shiny-notification-panel\"]" identifizieren und entsprechend testen.

Im Folgenden wird gezeigt, wie man in der Shiny-App eine Variable exportiert und Benachrichtigungen mittels showNotification() Benachrichtigungen darstellt:

Ein Test kann dann beispielsweise wie folgt aussehen: