日本Ruby会議2011の3日目の「テスティングフレームワークの作り方」の準備をしていますが、30分だと詰め込み過ぎになってしまうので、話さないことを事前に書いておきます。それは、テストを抽象化するためのAPIの違いです。
RSpecとtest-unit 2でのAPIの違いというと、class UserTest < Test::Unit::TestCase
とdescribe User
やassert
とshould
の違いの方が目に付きますが、抽象化するための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.
ざっくり訳すと、
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でテストを共有する場合は共有したいテストを書いたModule
をinclude
します。こちらは「テストの実装を共有する」と読める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のプログラムとしてどのように動くのがよいかという部分に集中できます。