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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
library(shiny) ui <- fluidRow(title = 'Minimal App', numericInput("num_input", "Bitte geben Sie eine Zahl n ein:", 0), textOutput('text_out') ) server <- function(input, output, session) { result <- reactive(input$num_input^2) output$text_out <- renderText( paste("Das Quadrat der Zahl n lautet: n² =", result()) ) } shinyApp(ui, server) |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
library(shinytest) library(testthat) context("Test Shiny-App") # shiny-App und PhantomJS öffnen app <- ShinyDriver$new("<Pfad zu app.R>") test_that("Output wird korrekt berechnet", { # num_input auf 30 setzen app$setInputs(num_input = 30) # text_out auslesen output <- app$getValue(name = "text_out") # testen expect_equal(output, "Das Quadrat der Zahl n lautet: n² = 900") }) # Shiny-App stoppen app$stop() |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
library(shiny) # gleiches ui wie oben ui <- fluidRow(title = 'Minimal App', numericInput("num_input", "Bitte geben Sie eine Zahl n ein:", 0), textOutput('text_out') ) server <- function(input, output, session) { result <- reactive(input$num_input ^ 2) output$text_out <- renderText( paste("Das Quadrat der Zahl n lautet: n² =", result()) ) # Initialisierung der exportierten Liste inputs_list <- c() observeEvent(input$num_input, { # neue Eingabe wird zu inputs_list zusammengefügt inputs_list <<- c(inputs_list, input$num_input) # Benachrichtigung zeigen showNotification(HTML(result()), duration = NULL) }) # inputs_list exportieren exportTestValues(inputs_list = {inputs_list}) } shinyApp(ui, server) |
Ein Test kann dann beispielsweise wie folgt aussehen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
library(shinytest) library(testthat) context("Test Shiny-App") # shiny-App und PhantomJS öffnen app <- ShinyDriver$new("<Pfad zu app.R>") test_that("inputs_list wird korrekt exportiert", { # Mehrere Eingaben app$setInputs(num_input = 1) app$setInputs(num_input = 7) app$setInputs(num_input = 42) # exportierte Variable inputs_list auslesen exported_list <- app$getAllValues()$export$inputs_list # testen (0 war der initiale Wert) expect_equal(exported_list, c(0, 1, 7, 42)) }) test_that("Benachrichtignungen beinhalten korrekten Text", { # HTML-Widget mit der XPath identifizieren popup <- app$findElement(xpath = "//*[@id=\"shiny-notification-panel\"]") # Benachrichtigungstext testen testthat::expect_equal(popup$getText(), "×\n0\n×\n1\n×\n49\n×\n1764") }) # shiny-App stoppen app$stop() |