ククログ

株式会社クリアコード > ククログ > test-unitならRSpec 3のComposable Matchers相当のことをどう書くか

test-unitならRSpec 3のComposable Matchers相当のことをどう書くか

RSpec 3の新機能であるComposable Matchersの使い方の例をtest-unitならどう書くか紹介します。リンク先のコードを示し、それのtest-unitバージョンを示す、という流れを繰り返します。

テスト対象

テスト対象は次のコードです。

class BackgroundWorker
  attr_reader :queue

  def initialize
    @queue = []
  end

  def enqueue(job_data)
    queue << job_data.merge(:enqueued_at => Time.now)
  end
end

キューの中身をチェック

RSpec 3ではComposable Matchersを使ってこう書くそうです。(リンク先から引用。以下同様。)

describe BackgroundWorker do
  it 'puts enqueued jobs onto the queue in order' do
    worker = BackgroundWorker.new
    worker.enqueue(:klass => "Class1", :id => 37)
    worker.enqueue(:klass => "Class2", :id => 42)

    expect(worker.queue).to match [
      a_hash_including(:klass => "Class1", :id => 37),
      a_hash_including(:klass => "Class2", :id => 42)
    ]
  end
end

test-unitではこう書きます。RSpecは「フレームワーク側が」必要な値だけ比較するという方針ですが、test-unitは「テストを書く側が」必要な値だけ取り出して比較するという方針です。

class BackgroundWorkerTest < Test::Unit::TestCase
  class EnqueueTest < self
    def test_order
      worker = BackgroundWorker.new
      worker.enqueue(:klass => "Class1", :id => 37)
      worker.enqueue(:klass => "Class2", :id => 42)

      assert_equal([
                     {:klass => "Class1", :id => 37},
                     {:klass => "Class2", :id => 42}
                   ],
                   normalize_queue(worker.queue))
    end

    private
    def normalize_queue(queue)
      queue.collect do |job_data|
        {
          :klass => job_data[:klass],
          :id    => job_data[:id],
        }
      end
    end
  end
end

リンク先ではテスト結果の失敗時にどのように報告するかについても触れています。enqueueの実装がコメントアウトされていたときを例にしています。

class BackgroundWorker
  # ...
  def enqueue(job_data)
    # queue << job_data.merge(:enqueued_at => Time.now)
  end
end

RSpec 3の場合は次のようになって読みやすい、ということです。英語で読みくだせるのがポイントですね。

1) BackgroundWorker puts enqueued jobs onto the queue in order
   Failure/Error: expect(worker.queue).to match [
     expected [] to match [(a hash including {:klass => "Class1", :id => 37}), (a hash including {:klass => "Class2", :id => 42})]
     Diff:
     @@ -1,3 +1,2 @@
     -[(a hash including {:klass => "Class1", :id => 37}),
     - (a hash including {:klass => "Class2", :id => 42})]
     +[]

   # ./spec/background_worker_spec.rb:19:in `block (2 levels) in <top (required)>'

test-unitの場合は次のようになります。RSpecとは対照的に、英語を極力排除して実際のプログラムとデータを見せる方に注力しています。

Failure:
test_order(BackgroundWorkerTest::EnqueueTest)
test-worker.rb:22:in `test_order'
     19:       worker.enqueue(:klass => "Class1", :id => 37)
     20:       worker.enqueue(:klass => "Class2", :id => 42)
     21: 
  => 22:       assert_equal([
     23:                      {:klass => "Class1", :id => 37},
     24:                      {:klass => "Class2", :id => 42}
     25:                    ],
<[{:id=>37, :klass=>"Class1"}, {:id=>42, :klass=>"Class2"}]> expected but was
<[]>

diff:
? [{:id=>37, :klass=>"Class1"}, {:id=>42, :klass=>"Class2"}]

Compound Matcher Expressions

Compound Matcher Expressionsという機能は次のように書ける機能ということです。これまではstart_withのチェックとend_withのチェックを別に書かなれければいけなかったのに、一緒に書けるようになったということです。

expect(alphabet).to start_with("a").and end_with("z")

test-unitではこう書きます。期待するパターンなら特定の文字列に置換します。一回で比較するという方針は同じです。

alphabet = "abcxyz"
a_z = "a...z"
assert_equal(a_z, alphabet.gsub(/\Aa.*z\z/, a_z))

次のように期待したパターンでない場合は元の文字列が変わらないので失敗します。

alphabet = "abcxyZ"
a_z = "a...z"
assert_equal(a_z, alphabet.gsub(/\Aa.*z\z/, a_z))

失敗時のメッセージにはちゃんと元の文字列がでるので、実際の値はなんだったのか、という情報が失われることはありません。

Failure:
test_a_z(AlphabetTest)
test-alphabet.rb:7:in `test_a_z'
     4:   def test_a_z
     5:     alphabet = "abcxyZ"
     6:     a_z = "a...z"
  => 7:     assert_equal(a_z, alphabet.gsub(/\Aa.*z\z/, a_z))
     8:   end
     9: end
<"a...z"> expected but was
<"abcxyZ">

diff:
? a...z 
?  bcxyZ

まとめ

RSpec 3の例はもっとたくさんありますが、2個だけ紹介しました。

値の比較の仕方に方針の違いがでていました。

  • RSpecは「フレームワーク側が」必要な値だけ比較するという方針
  • test-unitは「テストを書く側が」必要な値だけ取り出して比較するという方針

RSpec 3はより英語らしく読み書きできるようになりそうですね。

test-unitはあいかわらずRubyらしく読み書きできるテスティングフレームワークですね。