読者です 読者をやめる 読者になる 読者になる

Tbpgr Blog

Ruby プログラマ tbpgr(てぃーびー) のブログ

ひどいサンプルコードで property based testing をまなぶ

property based testing の概要とサンプルをまとめました。

example-based testing とは?

property based testing の前に対比として example-based testing について。

example-based testing は xUnit に代表されるもので、
入力シナリオ群を元に出力を検証します。

大抵の場合は手動でリストアップが必要で、それゆえに手間がかかり、
全ての検証対象をリストアップしきっているか信頼しにくいことが問題点としてあります。

property based testing とは?

入力を元に出力される結果が一定の規則を持っている場合に、
任意の入力ルールと、規則を指定することで
ランダムに生成される入力値を元に出力が規則通り行われているかを確認するテストです。

自分の説明に自信がないので ScalaTest - http://www.scalatest.org/user_guide/property_based_testing から引用すると

ScalaTest supports property-based testing, where a property is a high-level specification of behavior that should hold for a range of data points.

サンプル

Ruby の property-based test 用のライブラリ rantly で試します。
RSpec, Test::Unit, Mini::Testがサポートされています。
今回はRSpecを利用します。

サンプルとして String#upcase をテストします。
小文字を大文字化するこのメソッドは文字コードだと 32 減算したものになる、という性質を持ちます。
(upcase の実際の処理も同じことをやってるかもしれませんが、ここでは実処理は異なるものと仮定します。
テストの検証処理とテスト対象の実処理で同じことをテストしていたら意味がないので)

lower = (?a..?z).map(&:ord) # => [97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122]
upper = (?A..?Z).map(&:ord) # => [65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90]
lower.zip(upper).map{|e|e.first - e.last} # => [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32]

このルールでテストしてみます。

正常系のコード

require 'rantly/rspec_extensions'

RSpec.describe 'example' do
  it '小文字の文字列を大文字化した場合の文字コードは元の文字コードよりも32小さい' do
    property_of {
      string(:lower)[0]
    }.check(50) { |i|
      # デバッグ用
      puts "#{i.upcase}(#{i.upcase.ord}):#{(i.ord - 32).chr}(#{i.ord - 32})"
      expect(i.upcase).to eq((i.ord - 32).chr)
    }
  end
end
  • 実行結果
$ bundle exec rspec -fd spec/sample_spec.rb

example
L(76):L(76)

.T(84):T(84)
B(66):B(66)
N(78):N(78)
K(75):K(75)
J(74):J(74)
Q(81):Q(81)
Z(90):Z(90)
J(74):J(74)
G(71):G(71)
V(86):V(86)
.B(66):B(66)
A(65):A(65)
J(74):J(74)
S(83):S(83)
P(80):P(80)
F(70):F(70)
F(70):F(70)
Z(90):Z(90)
V(86):V(86)
D(68):D(68)
.C(67):C(67)
Q(81):Q(81)
N(78):N(78)
P(80):P(80)
K(75):K(75)
I(73):I(73)
W(87):W(87)
C(67):C(67)
P(80):P(80)
G(71):G(71)
.W(87):W(87)
Q(81):Q(81)
L(76):L(76)
N(78):N(78)
E(69):E(69)
H(72):H(72)
R(82):R(82)
T(84):T(84)
B(66):B(66)
C(67):C(67)
.Y(89):Y(89)
J(74):J(74)
Y(89):Y(89)
S(83):S(83)
Q(81):Q(81)
U(85):U(85)
J(74):J(74)
X(88):X(88)
I(73):I(73)

success: 50 tests
  小文字の文字列を大文字化した場合の文字コードは元の文字コードよりも32小さい

Finished in 0.00357 seconds (files took 0.09744 seconds to load)
1 example, 0 failures

異常系のコード

String をオーバーライドして h のときだけ いやん に変換されるようにします。
実装が適当ですが、あくまでサンプルなのでスルーでお願いします。

require 'rantly/rspec_extensions'

class String
  def upcase
    return 'いやん' if self == 'h'
    (self.ord - 32).chr
  end
end

RSpec.describe 'example' do
  it '小文字の文字列を大文字化した場合の文字コードは元の文字コードよりも32小さい' do
    property_of {
      string(:lower)[0]
    }.check(50) { |i|
      # デバッグ用
      puts "#{i.upcase}(#{i.upcase.ord}):#{(i.ord - 32).chr}(#{i.ord - 32})"
      expect(i.upcase).to eq((i.ord - 32).chr)
    }
  end
end
  • 実行結果
$ bundle exec rspec -fd spec/sample_spec.rb

example
D(68):D(68)

.J(74):J(74)
A(65):A(65)
I(73):I(73)
I(73):I(73)
B(66):B(66)
K(75):K(75)
F(70):F(70)
O(79):O(79)
F(70):F(70)
X(88):X(88)
.Z(90):Z(90)
I(73):I(73)
Q(81):Q(81)
G(71):G(71)
F(70):F(70)
J(74):J(74)
O(79):O(79)
K(75):K(75)
S(83):S(83)
S(83):S(83)
.L(76):L(76)
V(86):V(86)
B(66):B(66)
A(65):A(65)
M(77):M(77)
I(73):I(73)
T(84):T(84)
E(69):E(69)
E(69):E(69)
L(76):L(76)
.K(75):K(75)
Z(90):Z(90)
いやん(12356):H(72)

failure: 33 tests, on:
"h"
  小文字の文字列を大文字化した場合の文字コードは元の文字コードよりも32小さい (FAILED - 1)

Failures:

  1) example 小文字の文字列を大文字化した場合の文字コードは元の文字コードよりも32小さい
     Failure/Error: expect(i.upcase).to eq((i.ord - 32).chr)

       expected: "H"
            got: "いやん"

       (compared using ==)
     # ./spec/sample_spec.rb:16:in `block (3 levels) in <top (required)>'
     # ./vendor/bundle/ruby/2.4.0/gems/rantly-1.0.0/lib/rantly/property.rb:33:in `block in check'
     # ./vendor/bundle/ruby/2.4.0/gems/rantly-1.0.0/lib/rantly/generator.rb:83:in `generate'
     # ./vendor/bundle/ruby/2.4.0/gems/rantly-1.0.0/lib/rantly/property.rb:31:in `check'
     # ./spec/sample_spec.rb:14:in `block (2 levels) in <top (required)>'

Finished in 0.01015 seconds (files took 0.08968 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/sample_spec.rb:11 # example 小文字の文字列を大文字化した場合の文字コードは元の文字コードよりも32小さい

まとめ

一見 property-based testing のほうが便利そうにみえますが example-based testing を代替するわけではなく、
property-based testing を利用可能なケースに限って適用されるのかな、と思いました。
じゃあ、どんなケースが property-based testing を使うケースなのかと言われると説明しきれない私がいる。

関連資料