RubyKaigiで紹介されたGem「PBT(Property Based Testing)」を試してみた

こんにちは!ファインディでTeam+開発チームのEMをしている浜田です。

以前公開した記事「ファインディはRubyKaigi 2024 にPlatinum Sponsorsとして協賛します!」で紹介した通り、ファインディはRubyKaigi 2024に協賛しており、現地で参加してきました!

tech.findy.co.jp

今週(5/20〜25)はRubyKaigi 2024の振り返りも兼ねてRubyKaigiに関連した記事を投稿していきます!

この記事では、私が聞いたセッションの中の1つ「Unlocking Potential of Property Based Testing with Ractor 」で紹介されたGem「PBT」を試してみたので共有します。

Unlocking Potential of Property Based Testing with Ractor

Unlocking Potential of Property Based Testing with Ractor 」は、Property Based TestingをRubyで実行するためのGemを作成し、さらにRactorを活用することでテストパフォーマンスを向上させる話でした。

speakerdeck.com

セッションの中でも触れられていましたが、Property Based Testingを聞いたことがある方は20%ほどとのことだったので、この記事でも簡単に紹介したいと思います。

Property Based Testingでは、従来の入力値を開発者が列挙してテストする手法(セッションではExample Based Testingと呼んでいました)とは異なり、入力値の性質(プロパティー)を指定してテストケースを自動生成して網羅的なテストを行うことができます。

私は以前ファインディ主催で開催した「t-wadaさん、ymotongpooさんに聞くテスト戦略最前線」というイベントでt-wadaさんから紹介されて興味を持ちました。 スライドも公開されているので、興味がある方はぜひご覧ください。

speakerdeck.com

この記事では、Gem「PBT」を使ってProperty Based TestingをRubyで実装しつつ、従来のテスト手法であるExample Based Testingとの違いを比較します。

試してみた

それでは早速試していきます。 今回使用したRubyと主要なGemのバージョンは次の通りです。

  • Ruby 3.3.1
  • PBT 0.4.1
  • RSpec 3.13.0

今回は入力値を3倍にして返却する tripleというメソッドを実装します。単純にするため入力値は整数のみ考慮します。

class PropertyBasedTest
  def self.triple(value)
    # valueを3倍にして返す
  end
end

それではまずいつも通り入力値を開発者が列挙してテストするExample Based Testingでテストを書きます。なお、テストはRSpecを使用します。

tripleのテストを書く場合、皆さんはどのような入力値のパターンでテストを書きますか? 今回は、正の整数 / 負の整数 / 0 の3パターンのテストケースを書きました。 Example Based Testingの場合、開発者がテスト条件を満たす任意の値を選んでテストコードを書くため、適当に選んだ22 / -5 / 0を指定しました。

RSpec.describe PropertyBasedTest do
  describe '.triple' do
    context 'when example-based testing' do
      subject(:add) { described_class.triple(number) }

      context 'when input value is 22' do
        let(:number) { 22 }

        it { is_expected.to eq 66 }
      end

      context 'when input value is -5' do
        let(:number) { -5 }

        it { is_expected.to eq(-15) }
      end

      context 'when input value is 0' do
        let(:number) { 0 }

        it { is_expected.to eq 0 }
      end
    end
  end
end

実行結果は次の通り。無事テストが通りました🙌

PropertyBasedTest
  .triple
    when example-based testing
      when input value is 0
        is expected to eq 0
      when input value is 22
        is expected to eq 66
      when input value is -5
        is expected to eq -15

Finished in 0.1124 seconds (files took 4.22 seconds to load)
3 examples, 0 failures

ところが、tripleメソッドには未知のバグが埋め込まれていました。 3の倍数、もしくは3のつく入力値が与えられた場合は異常終了するというバグです。

今回のテストではたまたま3の倍数や3がつく数値をテストケースに含めていなかったため、このバグを見逃してしまいました。

このような見逃しを回避するための手法として、Property Based Testingが有効です。 Property Based Testingは、入力値の性質(プロパティー)を指定してテストケースを自動生成し、網羅的なテストを行うことができます。

では、PBTを使ってテストを書いてみましょう。 テストコードを見ていただければわかる通り入力値の具体的な値は指定しておらず、整数であること(Pbt.integer)のみを指定しています。

RSpec.describe PropertyBasedTest do
  describe '.triple' do
    context 'when property-based testing' do
      it do
        Pbt.assert(verbose: true) do
          # 入力値が整数であることを指定
          Pbt.property(Pbt.integer) do |number|
            result = described_class.triple(number)
            # 結果が3の倍数であることを確認
            expect(result).to eq number * 3
          end
        end
      end
    end
  end
end

実行結果は次の通り。正しくエラーが検出されました。

Randomized with seed 19396

PropertyBasedTest
  .triple
    when property-based testing
      example at ./spec/models/property_based_test_spec.rb:42 (FAILED - 1)

Failures:

  1) PropertyBasedTest.triple when property-based testing
     Failure/Error:
       Pbt.assert(verbose: true) do
         Pbt.property(Pbt.integer) do |number|
           result = described_class.triple(number)
           expect(result).to eq number * 3
         end
       end

     Pbt::PropertyFailure:
       Property failed after 5 test(s)
         seed: 113103392077172016306707556593348939592
         counterexample: 3
         Shrunk 8 time(s)
         Got RuntimeError: (´Д`) サンッ!!
           app/models/property_based_test.rb:5:in `triple'
           spec/models/property_based_test_spec.rb:45:in `block (6 levels) in <top (required)>'
           (長いので省略)

       Encountered failures were:
       - 614700
       - 307350
       - 153675
       - 76838
       - 38419
       - 4803
       - 301
       - 38
       - 3

       Execution summary:
       . √ -929894
       . √ 440110
       . √ -801140
       . √ -194044
       . × 614700
       . . × 307350
       . . . × 153675
       . . . . × 76838
       . . . . . × 38419
       . . . . . . √ 19210
       . . . . . . √ 9605
       . . . . . . × 4803
       . . . . . . . √ 2402
       . . . . . . . √ 1201
       . . . . . . . √ 601
       . . . . . . . × 301
       . . . . . . . . √ 151
       . . . . . . . . √ 76
       . . . . . . . . × 38
       . . . . . . . . . √ 19
       . . . . . . . . . √ 10
       . . . . . . . . . √ 5
       . . . . . . . . . × 3
       . . . . . . . . . . √ 2
       . . . . . . . . . . √ 1
       . . . . . . . . . . √ 0
     # ./spec/models/property_based_test_spec.rb:43:in `block (4 levels) in <top (required)>'

Finished in 0.08717 seconds (files took 4.23 seconds to load)
1 example, 1 failure

PBTではデフォルトで100通りのテストを実行します。 Property failed after 5 test(s)と表示されているので、5回目のテストでエラーが発生したとわかります。

counterexampleにはエラーが発生した入力値のうちShrunkして発見した最小の値が表示されています。今回は3が入力された場合にエラーが発生したことがわかります。 また、Shrunk 8 time(s)とのことなので、8回Shrunkして最小の値3を見つけたことがわかります。

無事バグが検出できたので、コードを修正して再度テストを実行します。 今回は無事テストが通りました🎉

PropertyBasedTest
  .triple
    when property-based testing
      example at ./spec/models/property_based_test_spec.rb:42

Finished in 0.09535 seconds (files took 4.28 seconds to load)
1 example, 0 failures

このようにProperty Based Testingを活用することで、開発者に依存した入力値ではなく、網羅的なテストを行うことができるため、開発者が予見できていないバグを見つけやすくなります。

今回のサンプルコードは単純な処理なのでテスト実行時間にほとんど差がありませんが、通常はPBTの方が試行回数が多くなるためテスト実行時間は長くなります。そのため、様々なインプットのパターンを網羅すべきテストケースなど適材適所で活用いきたいと思います!


5/28には「After RubyKaigi 2024〜メドピア、ZOZO、Findy〜」として、メドピア株式会社、株式会社ZOZO、ファインディ株式会社の3社でRubyKaigi 2024の振り返りを行います。

LTやパネルディスカッションなどコンテンツ盛りだくさんなのでぜひご参加ください!!

findy.connpass.com

ファインディでは、これからもRubyを積極的に活用して、Rubyとともに成長していければと考えております。

そして、一緒に働くメンバーを絶賛募集中です。

興味がある方はこちらから ↓

herp.careers