9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Shiny Appの継続的インテグレーション(1) : ShinytestとtestthatでShiny Appのテストを行う

Posted at

はじめに

Shiny Codeを書いていて、

  • Shiny Appもunit testを行いたい
  • testが通ったら本番環境にdeployしたい

となったときに、現状のベストプラクティスはなにかと探していて、方法が固まってきたので紹介します

この記事では、まずshinytesttestthatを使ってtestコードを書く方法をまとめます

shinytest とは?

RStudioが提供してるR packageであり、Shinyをtestするためのツールです。公式ドキュメントのProject Pageを見るのが一番わかりやすいです

日本語ですと、shinytestでShinyのテストを行う方法 で詳しく使い方を解説してくれています

testコードを作るには、shinytestから、Shiny Appを立ち上げ、一定の操作をして、snapshotを撮影するのを繰り返すことで、testコードを生成することができます。具体的には、以下のコードを実行してみるとShinyAppが立ち上がり、操作をファイルに保存してくれます

# install.packages("shinytest")
library(shinytest)
recordTest("path/to/app")

testthat とは?

testthatはRのコードの単体テストを行うためのR packageです。これについては、すでに多くのドキュメントがあるので、そちらを参考にしたほうがよいかと思います

実装例

A/B testで使用するSample Sizeを計算するShiny Appのコードをもとに説明します。 Githubはこちら : okiyuki99/ShinyAB

STEP 1. Shiny Appのtestを実行するスクリプトの準備

CIでスクリプトをKickすればTestが実行するようにしたいので、まずTestを実行する(Runする)コードrun_tests.Rを用意します

このときのディレクトリ構成は以下で準備します

$ tree -L 2
.

├── run_tests.R
├── server.R
├── tests
   ├── test-expected
   └── test.R
└── ui.R

tests/ディレクトリ配下に、実際のtestの内容(test.Rなど)を用意します。test-expected/は空のディレクトリでOKです

run_tests.Rはこのように書きます

library(testthat)
library(shinytest)
dir.create("tests/test-current", showWarnings = F)

testthat::test_that("Application works", {
  # Use compareImages = FALSE because the expected image screenshots were created
  shinytest::expect_pass(shinytest::testApp(".", testnames = NULL, quiet = F, compareImages = F))
})

3行目は、一時的にtestの結果を格納するディレクトクリtests/test-current/を準備します(testが終わると同時に無くなります)。shinytest::testApp(.)でtestを実行するapp.Rまたはui.R/server.Rが配置されているパスを指定します

ここでのポイントとして、compareImage=FALSEにしておくことです。デフォルトはTRUEなのですが、FALSEにすることで、ScreenshotのImageの比較を行わずに、生成するImageのメタ情報(jsonファイル)で比較することができるので、CIと相性が良いです

STEP 2. test codeを書く

実際のテストの中身を書いているtest.Rを用意します。
多いシチュエーションとしては、ボタンをClickしたときに正しい結果が返ってくるかのテストと、なんらかのinputIdの値を変更した場合のテストがあると思います

ここでは2つのtestを行います

  • 1つ目のtestが、デフォルトパラメータで"Go"ボタンを得られる結果が正しい結果かどうかを判定します
  • 2つ目のtestが、パラメータを変更し、Shiny UIが正しくアップデートされたかを判定します
library(testthat)
library(shinytest)

testthat::context("Shiny AB Test")

# open Shiny app and PhantomJS
app <- shinytest::ShinyDriver$new("../",  loadTimeout = 100000)

testthat::test_that("Click go", {
  
  # btn click
  app$setInputs(btn_go = "click")
  
  # Wait
  Sys.sleep(1)
  
  # Get data
  vals <- app$getAllValues()
  
  # Test
  testthat::expect_identical(vals$output$kable_proportion[[1]], 
                         '<table class=\"table table-striped table-bordered\" style=\"margin-left: auto; margin-right: auto;\">\n <thead>\n  <tr>\n   <th style=\"text-align:right;\"> UU </th>\n   <th style=\"text-align:right;\"> number_of_samples </th>\n   <th style=\"text-align:right;\"> alpha </th>\n   <th style=\"text-align:right;\"> power </th>\n   <th style=\"text-align:right;\"> test_method </th>\n   <th style=\"text-align:right;\"> as_is </th>\n   <th style=\"text-align:right;\"> to_be </th>\n   <th style=\"text-align:right;\"> sample_size_per_group </th>\n   <th style=\"text-align:right;\"> required_sample_size </th>\n   <th style=\"text-align:right;\"> sampling_rate </th>\n  </tr>\n </thead>\n<tbody>\n  <tr>\n   <td style=\"text-align:right;\"> 10,000 </td>\n   <td style=\"text-align:right;\"> 2 </td>\n   <td style=\"text-align:right;\"> 0.05 </td>\n   <td style=\"text-align:right;\"> 0.8 </td>\n   <td style=\"text-align:right;\"> prop.test </td>\n   <td style=\"text-align:right;\"> 0.3 </td>\n   <td style=\"text-align:right;\"> 0.4 </td>\n   <td style=\"text-align:right;\"> <span style=\"     color: blue !important;\">356</span> </td>\n   <td style=\"text-align:right;\"> <span style=\"     color: blue !important;\">712</span> </td>\n   <td style=\"text-align:right;\"> <span style=\"     color: blue !important;\">7.12%</span> </td>\n  </tr>\n</tbody>\n</table>'
  )
  
  # remove click
  app$setInputs(btn_remove = "click")

  # Get data
  vals <- app$getAllValues()
  
  # Test
  testthat::expect_null(vals$output$kable_proportion[[1]])
})

testthat::test_that("Change expected value and Update Lift", {
  
  # input
  app$setInputs(expected_value = 0.5)
  
  # Wait
  Sys.sleep(1)
  
  # Get data
  vals <- app$getAllValues()
  
  # Test 
  testthat::expect_identical(vals$output$ui_lift$html[[1]], 
                             '<div style="font-style: italic;">Lift from As-Is ratio to To-Be ratio :<strong> 66.67% </strong></div>')
})

# stop the Shiny app
app$stop()

shinytestのコードのポイントとなる箇所は以下の3点です

1. Shiny Appの立ち上げ

app <- shinytest::ShinyDriver$new("../", loadTimeout = 100000) でShinyDriver(Phantom JSで実装されているらしい) を使って、shiny appを立ち上げます

2. InputIdの操作・変更

app$setInputs(btn_go = "click") で shinyのinputIdがbtn_goの箇所をclickします。他にもapp$setInputs(expected_value = 0.5)でinputIdがexpected_valueのものをupdateします

3. 出力結果の比較

vals <- app$getAllValues()で現時点でのappが保有するRオブジェクトのvalueを取得(inputとoutputの内容など)し、testthatどおりに事前に用意した正解とtestthat::expect_identicalで比較します

(この例では、kableのhtml出力コードをベタッと比較しています。reactiveなdata.frameを用意して、比較することもできると思います)

STEP 3. testを実行する

ここまででコードは用意できたので、R -f run_tests.Rでtestを実行してみましょう。Errorがなければ、そのまま完了するかと思います

おわりに

shinytesttestthatを使ったShinyのunit testの方法を紹介しました。次は、Travis CIからtestを実行して、testが成功したら、shinyapps.ioに自動でデプロイする方法を紹介します

参考

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?