メモ:DBItestを通す

DBIを使ったパッケージを実装したい人のためのお役立ち情報(超ニッチ)その2です。その1はこれ。

DBItestとは

DBIに準拠していることをテストするために、DBItestというパッケージが用意されています。

DBItestをインストールすれば、おめでとうございます、これであなたもDBIとのつらい戦いのスタートラインに立ったことになります。

DBItestの使い方

使い方はこのvignetteが詳しいです。

まずはDBItest用のtestthatのテストを追加しましょう。例ではDBItestになってますが、なんでも大丈夫です。

devtools::use_testthat()
devtools::use_test("DBItest")

これを実行すると、tests/testthat/test-DBItest.Rというファイルができているはずです。開いて、中身をまるごと以下に置き換えましょう。<ドライバのコンストラクタ>()というのはSQLite()とかそういうやつです。

DBItest::make_context(<ドライバのコンストラクタ>(), NULL)
DBItest::test_all()

え、たったの2行でいいの??と思うかもしれませんが、たったの2行でいいんですけど、DBItest::test_all()で流れるフルスペックのテストを通るようなDBパッケージは稀です。たぶんGitHubにログインした状態じゃないと見れませんが、以下から名だたるパッケージのtest-DBItest.Rを眺めると、苦労の多さが偲ばれることでしょう。

ということで、いきなり全部のテストを流すのではなく、まずは簡単なところから始めましょう。DBItest::test_all()の中身を見れば以下のようになっているのがわかります。

DBItest::test_all
#> function (skip = NULL, ctx = get_default_context()) 
#> {
#>     test_getting_started(skip = skip, ctx = ctx)
#>     test_driver(skip = skip, ctx = ctx)
#>     test_connection(skip = skip, ctx = ctx)
#>     test_result(skip = skip, ctx = ctx)
#>     test_sql(skip = skip, ctx = ctx)
#>     test_meta(skip = skip, ctx = ctx)
#>     test_transaction(skip = skip, ctx = ctx)
#>     test_compliance(skip = skip, ctx = ctx)
#> }
#> <environment: namespace:DBItest>

ひとまずtest_driver()だけからはじめるといいでしょう。具体的には上のtest-DBItest.Rをこう書き換えます。

DBItest::make_context(<ドライバのコンストラクタ>(), NULL)
DBItest::test_getting_started()
DBItest::test_driver()

これで、test_driver()が通ったら次はtest_connect()、その次はtest_result()...というようにしてテストを足していきましょう。

tweak

DBItestには、テストケースをいじらないといけない場合としてよくあるものをDBItest::make_context()tweak引数に指定できます。この引数に指定するオブジェクトはDBItest::tweak()で作成します。

例えば、Redashrでまず引っかかったのは、こんなエラーです。RedashrパッケージのドライバのコンストラクタはRedash()なんですが、なぜかedash()という関数を使おうとして「そんなものはない」というエラーになっています。

Failed -------------------------------------------------------------------------
1. Failure: DBItest: Driver: constructor ---------------------------------------
"edashr" %in% getNamespaceExports(pkg_env) isn't true.


2. Failure: DBItest: Driver: constructor ---------------------------------------
exists("edashr", mode = "function", pkg_env) isn't true.


3. Error: DBItest: Driver: constructor -----------------------------------------
object 'edashr' of mode 'function' was not found

これは、「DBI対応のパッケージは「R + <DB名>」という名前なので、コンストラクタはパッケージ名の先頭一文字を取ったものだ!」という暗黙の前提があるためです(そんなのどこに書いてあるんだ...)。

このエラーに対処するためには、tweak()constructor_nameを指定します。具体的にはこうなります。

tweaks <- DBItest::tweaks(
  constructor_name = "Redash"
)

DBItest::make_context(
  Redash(),
  NULL,
  tweaks = tweaks
)

他にもいろんなtweakがあるので詳しくはヘルプを参照してみてください。ちなみにRedashrで今指定しているtweakはconstructor_relax_argsomit_blob_testsです。参考までに。

construct_args

さて、DBをテストするには、テスト用のDBに接続したりします。当たり前ですけど。しかし、常にデフォルト引数で接続できるとは限りません。そういときは、DBItest::make_context()construct_args引数に必要な引数を指定できます。

DBItest::make_context(
  SuperGreatDB(),
  connect_args = list(host = "127.0.1.1", password = "foo"),
  tweaks = tweaks
)

また、環境による違いもあります。例えばRedashrでは、手元とCIでIPアドレスやポートが違います。こういう違いを吸収するものとして、configパッケージが便利です。

suryu.me

Redashrではこういうconfig.ymltests/testthat/下(プロジェクトルートではない点に注意)に置いて、

default:
  connect_args:
    api_key: L3BcIMTltQoTB83mGtHrcUa8mmeJdMaTuGFA3Bkv
    base_url: http://10.0.75.1/
    data_source_name: test

circleci:
  connect_args:
    api_key: L3BcIMTltQoTB83mGtHrcUa8mmeJdMaTuGFA3Bkv
    base_url: http://127.0.0.1:5000/
    data_source_name: test

CI上だけで定義される環境変数で設定を分岐しています。(まあこれはbase_urlしか違わないので、configパッケージを使うほどでもないのかもしれませんけど)

if (identical(Sys.getenv("CIRCLECI"), "true")) Sys.setenv(R_CONFIG_ACTIVE = "circleci")

DBItest::make_context(
  Redash(),
  connect_args = config::get("connect_args"),
  tweaks = tweaks
)

skip

そうはいっても、がんばっても通らないテストというものがあります。こういうものは、各テストのskip引数に指定しておくとスキップできます。

たとえば、Redashrではdata_type_driverというテストをスキップ指定しています。

DBItest::test_driver(
  skip = c(
    # Redash cannot determine the type of backend before connecting
    "data_type_driver"
  )
)

困ったときは仕様を確認

なぜこのテストが落ちるのか、とか困ったときには仕様を確認しましょう。

そうすれば、デフォルトの実装の方が間違っていることにも気付いたりできます(つらい)。

感想

DBItestは、ドキュメントがちゃんとしているようで色んな所に暗黙知みたいなのがあるのが難しいところですね...。ここに書いた内容があってるのかもあんまり自信がないので、変なとこがあればツッコミください。