RSpecとtest-unit 2での抽象化したテストの書き方の違い - 2011-07-12 - ククログ

ククログ

株式会社クリアコード > ククログ > RSpecとtest-unit 2での抽象化したテストの書き方の違い

RSpecとtest-unit 2での抽象化したテストの書き方の違い

日本Ruby会議2011の3日目の「テスティングフレームワークの作り方」の準備をしていますが、30分だと詰め込み過ぎになってしまうので、話さないことを事前に書いておきます。それは、テストを抽象化するためのAPIの違いです。

RSpecとtest-unit 2でのAPIの違いというと、class UserTest < Test::Unit::TestCasedescribe Userassertshouldの違いの方が目に付きますが、抽象化するためのAPIにもツールの特徴が出ています。抽象化するためのAPIはテストの量が増えてくると必要になる大事な機能です。ここでは、その中でも「テストを共有するAPI」について考えます。

まず、ツールの考え方について確認し、その後、それぞれのツールでどのようなAPIになっているかをみます。

ツールの考え方

まず、それぞれのツールの考え方について確認しましょう。

RSpecの考え方

RSpecでの書き方を見る前に、RSpecがどういうことを実現するためのツールとして開発されているかを確認しましょう。

BDD is an approach to software development that combines Test-Driven Development, Domain Driven Design, and Acceptance Test-Driven Planning. RSpec helps you do the TDD part of that equation, focusing on the documentation and design aspects of TDD.

RSpec+Documentation

ざっくり訳すと、

BDDはテスト駆動開発とドメイン駆動設計と受け入れテスト駆動計画づくりとATDPを合わせたソフトウェア開発の方法で、RSpecはそのうちのテスト駆動開発の部分だけをお手伝いしますよ。もう少し言うと、テスト駆動開発の特徴であるドキュメントと設計の部分を特に重視しています。

となります1

ドキュメントと設計(とRSpecというツール名)を合わせて考えると、仕様をテストとして実行できる形にしながら開発を進めたいのではないかという解釈ができます。そうすると、仕様とテストを一緒に書きやすいAPIを目指しているはずです。

test-unit 2の考え方

test-unit 2はxUnit系のテスティングフレームワークです。Rubyで書かれたコードのテストをRubyで書けることを重視しています2。そのため、Rubyとしてテストを書きやすいAPIを目指しています。

書き方

それでは、このような考え方を持つツールはどのようなAPIを提供するかを見てみましょう。

RSpecでの書き方

RSpecでテストを共有する場合はit_behaves_likeを使います。以下は、shared examples - Example Groups - RSpec Coreにあるコードです。「別途記述した動作通りに動くこと」と読めるAPIになっていますね。期待した動作を取り込むのではなく、参照しているように読めるところがポイントです。

require "set"

shared_examples "a collection" do
  let(:collection) { described_class.new([7, 2, 4]) }

  context "initialized with 3 items" do
    it "says it has three items" do
      collection.size.should eq(3)
    end
  end

  describe "#include?" do
    context "with an an item that is in the collection" do
      it "returns true" do
        collection.include?(7).should be_true
      end
    end

    context "with an an item that is not in the collection" do
      it "returns false" do
        collection.include?(9).should be_false
      end
    end
  end
end

describe Array do
  it_behaves_like "a collection"
end

describe Set do
  it_behaves_like "a collection"
end

test-unit 2での書き方

test-unit 2でテストを共有する場合は共有したいテストを書いたModuleincludeします。こちらは「テストの実装を共有する」と読めるAPIになっていますね。RubyではModuleは実装を共有する手段として提供されているため、それをそのまま「テストを共有」するために使っていることがポイントです。

require "set"

gem "test-unit"
require "test/unit"

module CollectionTests
  def collection
    @collection ||= collection_class.new([7, 2, 4])
  end

  def test_initilize
    assert_equal(3, collection.size)
  end

  def test_include_true
    assert_true(collection.include?(7))
  end

  def test_include_false
    assert_false(collection.include?(9))
  end
end

class ArrayTest < Test::Unit::TestCase
  include CollectionTests

  def collection_class
    Array
  end
end

class SetTest < Test::Unit::TestCase
  include CollectionTests

  def collection_class
    Set
  end
end

まとめ

テスティングフレームワークの作り方からもれた話題のひとつである「 RSpecとtest-unit 2の考え方の違いとそれが『テストを共有するAPI』にどう現れているか」をみてみました。

考え方としてRSpecとtest-unit 2のどちらがよいかではなく、自分がやろうとしている作業にはどちらが合っているかを考えるべきです。仕様としても使えるテストとRubyとして書けるテストのどちらが必要か・重要かを考えます。

例えば、すでにある仕様書や要望リストを実現するために作業している場合はRSpecの方が作業に合っているかもしれません。仕様っぽいAPIである程度Rubyと切り離して作業することにより、仕様を意識しながら作業を進めることができます。

そうではなく、内部で使うためのもので外部とのインターフェイスとなっていない部分であれば、test-unit 2の方が合っているかもしれません。RubyのそのままのAPIを使ってテストを書くため、仕様としてどうかということよりも、Rubyのプログラムとしてどのように動くのがよいかという部分に集中できます。

  1. テスト駆動開発がドキュメントを重視していたかどうかを覚えていないので、訳し間違いかもしれません。

  2. それとテストを自動化できること。