💯

RBSのテスト方法4パターン

2023/12/09に公開

RBSのテスト

これまで、「RBSのテスト」という文脈で語られた文章を見たことがないので、私の2年以上のRBS研究の経験からわかってきたことをまとめようと思う。

「RBSのテスト」とは、RBSを記載した場合に「私が書いたRBSはこれでいいのかな?」に答えるものである。
利用可能なRBSなのか、実装に即した出鱈目なものじゃないか、矛盾はないかなどを確認できるものである。

私はこのRBSのテストというテーマで4つの方法が現在あるだろうと考えている。4つとは以下のものである。

  • rbs validate
  • steep check
  • RBS::Test
  • RBS::UnitTest

この4つは、「動的か静的か」と「カジュアルか重厚か」という2軸の4象限でまとめることができる。

静的 動的
カジュアル rbs validate RBS::Test
重厚 steep check RBS::UnitTest

rbs validate

rbs validaterbs gemが提供するCLIコマンドだ。
rbs -I sig validateのように対象の型ディレクトリーを任意に選択できる。
RBSファイルを全て読み込み、RBSファイルだけを見た時のおかしなところがないかを調べることができる。例えば、シンタックスチェックもちろん、メソッドに2重定義はないか、alias先のメソッドは存在するか、classをincludeしてないか等の異常もチェックしてくれる。
静的かつカジュアルに使えるため、RBSを使用する全ての状況で利用を推奨できる活用範囲の広い汎用的なツールだ。なにはともあれrbs validateと覚えよう。
rbs validateが通ることで、RBSのシンタックスが通っていることはもちろん、矛盾がなく、各種RBSツールで利用可能であることを保証できる。CIに組み込むことも容易で、既存のプロジェクトにRBSを導入した場合でも最初からの利用を推奨する。

RBS::Test

RBS::Testはrbs gemが提供するclass名だが、その実態は既存のminitestやrspec等のテストを走らせた時に、ついでにRBS定義に則っているかもチェックできるというものである。
これを利用している有名なものは、最近出てきたHTTP Clientライブラリーhttpxが挙げられる。
RBS::Testでは環境変数を設定するだけで既存のテストにRBSの型チェックを組み込むことができる。
既存の資産を利用できるのでカジュアル、テストを走らせるので動的と位置付けられる。

steep check

静的型チェックツールsteepを利用する方法である。
steepでチェックする対象は2つあり、実装と利用とがある。ライブラリーとアプリケーションとも言える。
実装の例は

class Foo
  def foo
  end
end

とするなら、利用は

f = Foo.new
f.foo

となる。

steepのチェック対象をどちらにするかで話は変わってくる。

実装を対象とする場合

この場合はrubocopのような運用になると思われる。追加でコードが必要ないく使いやすいが、現状型付けが不可能な場合も存在するため、全項目100%達成は難易度が高い。開発の補助としてはライブラリーレベルならかなり優秀な相棒だが、Railsアプリケーションレベルとなると未開の地となる。

利用を対象とする場合

サンプルコード的なものを書いて、この範囲でなら100%達成を目指すもの。これはgem_rbs_collectionで行っているアプローチでもある。RBSの利用を確認できるため有効な手段だが、追加でコードが必要となるので重厚に分類される。

RBS::UnitTest

これは最近追加された(リリースはrbs v3.4予定)rbsのclass。rbsリポジトリには型のテストコードとして、実際にコードを実行して設定している型の範疇に入っているかを確かめているが、これを一般化したものになる。
サンプルとしては次のように書く。
Fooclassのbarインスタンスメソッドで() -> Stringを期待するが、この挙動をするか、そもそもこの挙動は読み込んでいるRBSの記述の範疇に入っているかを動的に確認できる。

assert_send_type, "() -> String", Foo.new, :bar

型が出鱈目じゃないかを実際に確かめているので確実性は高いが、追加でテストコードを書く必要があるので、効果は高いが重厚な手段となる。

まとめ

なにはともあれrbs validateは入れておいて損はないだろう。これは確実に使われる手段だと思われる。
カジュアルな手段であるRBS::Testも選択肢に入ってくると思う。追加コードを書くことなく、RBSが出鱈目じゃないかを確認できるので使いやすい。
しかし、これもチェック不可能なパターン(Delegatorとか)が存在するため、rbs validateほどの必須要件でもない。
steepは人気の手段で、そもそもオートコンプリートやホバー動作が便利なので導入はお薦めするが、テスト用途として使うなら、ある程度コストかけてガッツリと型と向き合うことになる。
RBS::UnitTestは出たての手段だが、そもそも標準ライブラリーの型チェックをライブラリー本体へ移すために使われているので、ライブラリー用途として使いやすい手段だと思われる。

以上RBSのテストという切り口でより良い型生活を考えてみた。この話題が一般化する日に、この記事が役立つといいな。

Discussion