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

## はじめに

Shiny Codeを書いていて、

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

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

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

## `shinytest` とは？

RStudioが提供してるR packageであり、Shinyをtestするためのツールです。公式ドキュメントの[Project Page](https://rstudio.github.io/shinytest/)を見るのが一番わかりやすいです

日本語ですと、[shinytestでShinyのテストを行う方法](https://www.randpy.tokyo/entry/shiny_28) で詳しく使い方を解説してくれています

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

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

## `testthat` とは？

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

* [Project Page](https://testthat.r-lib.org/)
* [R packages by Hadley Wickham - Testing](http://r-pkgs.had.co.nz/tests.html)


## 実装例

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

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

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

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

```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`はこのように書きます

```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が正しくアップデートされたかを判定します

```R
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がなければ、そのまま完了するかと思います

## おわりに

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

## 参考 
* [Using shinytest with continuous integration](https://rstudio.github.io/shinytest/articles/ci.html)
* [Unit testing shiny apps using testthat
](https://blog.daqana.com/en/unit-testing-shiny-apps-using-testthat/)