はじめに
ソフトウェアを作るときには同時にテストも作ります。 テストを動かすことで、ソフトウェアが設計の通り動作しているかを確認できます。もし設計の通りに動作しない場合はテストが失敗し、ソフトウェアに期待する動作と現在の間違った動作が明確になります。 テストをすっきりと書くことができると、テストを読みやすくなり、また、きれいなソースコードのままで新しくテストを追加することができます。 今回は、そのすっきりとテストを書くための方法について説明します。
テストを追加していくと発生する問題
例えば、1つのテストケースの中にいろいろな機能のテストがある場合を考えます。 ここで、ある機能の実装を修正したので、この機能に関するテストを追加しようとしました。 テスト名に「テストのコンテキスト」と「テスト対象」を含めてどのような内容のテストかを示します。 このとき、ある機能に対して様々な動作をテストすることも多いため、すでにテストケースの中に追加したいテストとコンテキストを共有しているテストがある場合もあります。 ここで問題が発生します。すべてのテスト名にコンテキストを含めるとテスト名が長くなってしまいます。 さらに、これを色んなコンテキストで繰り返すことで、同じコンテキスト名を含んだテスト名が複数あると、同じ情報が何度も現れて見にくくなります。
テストのグループ分け
この問題を解決する方法がテストをクラスでグループ分けする方法です。 グループ分けすることで、ごちゃごちゃしたテストをすっきりと整頓できます。 つまり、テスト名を短く、またすっきりとした状態を保ったままテストを追加できるようになります。
実際にクラスによるグループ分けをどうやるかについて、全文検索エンジンgroongaのRubyバインディングであるrroongaのテストを例に挙げて説明します。 rroongaはgroongaをRubyから便利に使うためのライブラリで、内部でgroongaを使用しています。 ここでは1つのデータの集まりを表すレコードオブジェクトに関するテストを例にします。
以下はレコードに関するテストの一部です。 テスティングフレームワークはtest-unitを使っています。
class RecordTest < Test::Unit::TestCase
include GroongaTestUtils
def test_have_column_id
groonga = @bookmarks.add
assert_true(groonga.have_column?(:_id))
end
def test_have_column_key_hash
mori = @users.add("mori")
assert_true(mori.have_column?(:_key))
end
def test_have_column_key_array_with_value_type
groonga = @bookmarks.add
assert_true(groonga.have_column?(:_key))
end
def test_have_column_key_array_without_value_type
groonga_ml = @addresses.add
assert_false(groonga_ml.have_column?(:_key))
end
def test_attributes
values = {
"uri" => "http://groonga.org/",
"rate" => 5,
"comment" => "Grate!"
}
groonga = @bookmarks.add(values)
assert_equal(values.merge("_id" => groonga.id,
"content" => nil,
"user" => nil),
groonga.attributes)
end
def test_recursive_attributes
@bookmarks.define_column("next", @bookmarks)
top_page_record = @bookmarks.add(top_page)
doc_page_record = @bookmarks.add(doc_page)
top_page_record["next"] = doc_page_record
doc_page_record["next"] = top_page_record
expected = {
"_id" => 1,
"user" => nil,
"uri" => "http://groonga.org/",
"rate" => 5,
"next" => {
"_id" => 2,
"user" => nil,
"uri" => "http://groonga.org/document.html",
"rate" => 8,
"content" => nil,
"comment" => "Informative"
},
"content" => nil,
"comment" => "Great!"
}
expected["next"]["next"] = expected
assert_equal(expected, top_page_record.attributes)
end
def test_key
documents = Groonga::PatriciaTrie.create(:name => "Documents",
:key_type => "ShortText")
reference = documents.add("reference")
assert_equal("reference", reference.key)
end
def test_value
bookmark = @bookmarks.add
assert_equal(0, bookmark.value)
bookmark.value = 100
assert_equal(100, bookmark.value)
end
end
説明のために、上のソースコードからテストケース名とテスト名だけ抜き出して並べます。
class RecordTest
def test_have_column_id; end
def test_have_column_key_hash; end
def test_have_column_key_array_with_value_type; end
def test_have_column_key_array_without_value_type; end
def test_attributes; end
def test_recursive_attributes; end
def test_key; end
def test_value; end
end
テスト名を見ると、「どんなカラムを持っているか」という「コンテキスト(have_column)」のテストが4つ、「属性はどんな状態か」という「コンテキスト(attributes)」のテストが2つあることがわかります。 しかし、「どんなカラムを持っているか」という「コンテキスト(have_column)」をもつテスト4つ全てに、コンテキスト(have_column)を示す名前が入っています。 「属性はどんな状態か」という「コンテキスト(attributes)」をもつテストにも同じようにコンテキスト(attributes)を示す名前が入っているため、同じ情報が何度も現れて見にくくなる問題が発生しています。
そこで、「どんなカラムを持っているか」という「コンテキスト(have_column)」と、「属性はどんな状態か」という「コンテキスト(attributes)」でテストをグループ分けします。 まず、「have_column」をコンテキストをグループ分けしましょう。 対象となるのは以下のテストです。
class RecordTest
def test_have_column_id; end
def test_have_column_key_hash; end
def test_have_column_key_array_with_value_type; end
def test_have_column_key_array_without_value_type; end
end
まず、これらのテストをコンテキスト名を含んだクラスに移動します。
class RecordTest
class HaveColumnTest
def test_have_column_id; end
def test_have_column_key_hash; end
def test_have_column_key_array_with_value_type; end
def test_have_column_key_array_without_value_type; end
end
end
コンテキスト名を含んだクラスは移動前のクラスを継承します。 継承することで、自然と移動前のコンテキストを引き継いだテストを書くことができるからです。
class RecordTest
class HaveColumnTest < self
def test_have_column_id; end
def test_have_column_key_hash; end
def test_have_column_key_array_with_value_type; end
def test_have_column_key_array_without_value_type; end
end
end
selfはクラスの定義中だとそのクラスとなるため、ここでのselfはRecordTestになります。 最後に、テスト名からコンテキストを除きます。 すでにクラス名にコンテキストが含まれているため、コードでコンテキストを表現できているからです。
class RecordTest
class HaveColumnTest < self
def test_id; end
def test_key_hash; end
def test_key_array_with_value_type; end
def test_key_array_without_value_type; end
end
end
このように分類することで、コンテキスト(have_column)を表すのがクラス名だけになり、同じ情報が何度も現れる問題を解決することができました。
同じようにattributesのコンテキストについてもやってみましょう。 対象となるのは以下のテストです。
class RecordTest
def test_attributes; end
def test_recursive_attributes; end
end
まず、これらのテストをコンテキスト名を含んだクラスに移動します。
class RecordTest
class AttributesTest
def test_attributes; end
def test_recursive_attributes; end
end
end
コンテキスト名を含んだクラスは移動前のクラスを継承します。
class RecordTest
class AttributesTest < self
def test_attributes; end
def test_recursive_attributes; end
end
end
最後に、テスト名からコンテキストを除きます。
class RecordTest
class AttributesTest < self
def test_single; end
def test_recursive; end
end
end
test_attributesは、attributesというコンテキストでは単一の属性についてテストしているので、ここでは新しくtest_singleとしました。 コンテキストがattributesのテストに関しても、attributesというコンテキストが何度も現れる問題が解決されています。 グループ分けをした後のRecordテストは次のようになります。
class RecordTest
class HaveColumnTest < self
def test_id; end
def test_key_hash; end
def test_key_array_with_value_type; end
def test_key_array_without_value_type; end
end
class AttributesTest < self
def test_single; end
def test_recursive; end
end
def test_key; end
def test_value; end
end
コンテキストをクラス名に移動することで、関連するテストをまとめることができました。これにより、テストコード全体がすっきりしました。 今後、テストを追加するときも同じコンテキスト名を重複して書かなくてもよくなるため、すっきりした状態を保つことができます。
まとめ
テストをすっきりさせる方法としてテストをグループ分けする方法を紹介しました。 すっきりした状態ではテストも読みやすくなります。 また、きれいなソースコードのままテストを追加することもできます。
おまけ
今回の例に挙げたテストは、RSpecではdescribeを使って書くことができます。
describe Groonga::Record do
describe "#have_column?" do
it "should true for _id" do; end
it "should true for _key of hash" do; end
it "should true for _key of array that has value type" do; end
it "should false for _key of array that doesn't have value type" do; end
end
describe "#attributes" do
it "should return hash for record that doesn't have reference" do; end
it "should return hash for record that has recursive reference" do; end
end
it "should get key" do; end
it "should get and set value" do; end
end