テストをすっきり書く方法 - 2012-04-25 - ククログ

ククログ

株式会社クリアコード > ククログ > テストをすっきり書く方法

テストをすっきり書く方法

はじめに

ソフトウェアを作るときには同時にテストも作ります。 テストを動かすことで、ソフトウェアが設計の通り動作しているかを確認できます。もし設計の通りに動作しない場合はテストが失敗し、ソフトウェアに期待する動作と現在の間違った動作が明確になります。 テストをすっきりと書くことができると、テストを読みやすくなり、また、きれいなソースコードのままで新しくテストを追加することができます。 今回は、そのすっきりとテストを書くための方法について説明します。

テストを追加していくと発生する問題

例えば、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