株式会社クリアコード > ククログ

ククログ

最新
タグ:

Firefox/Thunderbird用アドオン開発者向けテスティングフレームワーク UxU 1.0.0をリリースしました

長らく更新がない状態が続いてしまっておりましたが、Firefox/Thunderbird用アドオンの開発者向けテスティングフレームワークUxU(UnitTest.XUL)の新バージョンとなるバージョン1.0.0を、本日付けでリリースしました。動作対象は、現在Mozillaによって公式にサポートが継続されているFirefox 10 ESR、Thunderbird 10 ESR、および最新のリリース版までです。Firefox 3.6、Thunderbird 3.1などの旧バージョンでの動作は保証されていませんので、ご注意下さい。

テストの並列実行

バージョン1.0.0では影響の大きな変更点として、複数のテストの並列実行に関する動作が大幅に改善され、実用的な機能になりました。

設定画面から最大の並列実行数を変更するか、コマンドライン引数を通じて並列実行数を明示的に指定することで、これまですべてシーケンシャルに実行されていたテストが、テストケース(ファイル)単位で並列に実行されるようになります。

並列実行が有効な場面

JavaScriptを使ったテストでは、Webページの読み込みなど、同期的には結果を得られない・処理待ちが必要となる場面が多くあります。UxUでは、ユーティリティメソッドなどを通じて簡単に処理待ちを行える事が特長の1つとなっています。

しかしながら、処理待ちが発生するという事は、必然的に、全体のテスト実行時間が長くなってしまう事にも繋がります。

複数のテストを並列実行すると、1つのテストで処理待ちしている間に、別のテストの処理を進める事ができます。処理待ちしている時間を無駄にせずに済むようになるため、処理待ちが多い場合においては、全体的なテスト実行時間の短縮が期待できます。

並列実行が有効でない場面

UxUは、プロファイルを指定して別プロセスを起動する場合を除き、すべてのテストが同一プロセスの同一スレッド上で動作する設計となっています。そのため、単純なforループやwhileループなど、同期的な処理により時間がかかってしまっている場面については、他のテストを並列実行する事ができません。

このような理由でテストの実行時間が長くなっているケースについては、残念ながら、並列実行では実行時間の短縮は望めません。

並列実行できない場面

テストの設計によっては、他のテストと並列に実行するとランダムに失敗するようになってしまう場合があります*1。並列実行機能は、個々のテスト同士が衝突しないようになっている事を確認した上で利用して下さい。

なお、テストケース中で「var parallel = false;」と明示的に指定する事により、そのテストケースを並列実行の対象外にする事ができます。並列実行の対象外になっているテストケースは、同時に2つ以上実行される事はありません*2。並列実行すると失敗するテストについてはこの方法で問題を回避し、できる所から並列実行の恩恵を得るという事もできます。

まとめ

UxU バージョン1.0.0で実用的な機能となった複数テストの並列実行機能について、有効な場面と有効でない場面、問題が起こる場面などの情報を簡単にご紹介しました。テスト実行時間の長さに悩まされていた方は、是非一度お試し下さい。

*1  例えば、単一のテスト用フレームを2つのテストで同時に使用してしまっている場合や、複数のテストでそれぞれウィンドウを開いていて「最前面にあるウィンドウ」を取り合ってしまう場合など。

*2  並列実行の対象になっているテストが他にある場合は、それとは並行して実行されます。

タグ: Mozilla | テスト | UxU
2012-07-24

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

はじめに

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

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

例えば、1つのテストケースの中にいろいろな機能のテストがある場合を考えます。 ここで、ある機能の実装を修正したので、この機能に関するテストを追加しようとしました。 テスト名に「テストのコンテキスト」と「テスト対象」を含めてどのような内容のテストかを示します。 このとき、ある機能に対して様々な動作をテストすることも多いため、すでにテストケースの中に追加したいテストとコンテキストを共有しているテストがある場合もあります。 ここで問題が発生します。すべてのテスト名にコンテキストを含めるとテスト名が長くなってしまいます。 さらに、これを色んなコンテキストで繰り返すことで、同じコンテキスト名を含んだテスト名が複数あると、同じ情報が何度も現れて見にくくなります。

テストのグループ分け

この問題を解決する方法がテストをクラスでグループ分けする方法です。 グループ分けすることで、ごちゃごちゃしたテストをすっきりと整頓できます。 つまり、テスト名を短く、またすっきりとした状態を保ったままテストを追加できるようになります。

実際にクラスによるグループ分けをどうやるかについて、全文検索エンジンgroongaのRubyバインディングであるrroongaのテストを例に挙げて説明します。 rroongaはgroongaをRubyから便利に使うためのライブラリで、内部でgroongaを使用しています。 ここでは1つのデータの集まりを表すレコードオブジェクトに関するテストを例にします。

以下はレコードに関するテストの一部です。 テスティングフレームワークはtest-unitを使っています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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

説明のために、上のソースコードからテストケース名とテスト名だけ抜き出して並べます。

1
2
3
4
5
6
7
8
9
10
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」をコンテキストをグループ分けしましょう。 対象となるのは以下のテストです。

1
2
3
4
5
6
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

まず、これらのテストをコンテキスト名を含んだクラスに移動します。

1
2
3
4
5
6
7
8
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

コンテキスト名を含んだクラスは移動前のクラスを継承します。 継承することで、自然と移動前のコンテキストを引き継いだテストを書くことができるからです。

1
2
3
4
5
6
7
8
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になります。 最後に、テスト名からコンテキストを除きます。 すでにクラス名にコンテキストが含まれているため、コードでコンテキストを表現できているからです。

1
2
3
4
5
6
7
8
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のコンテキストについてもやってみましょう。 対象となるのは以下のテストです。

1
2
3
4
class RecordTest
  def test_attributes; end
  def test_recursive_attributes; end
end

まず、これらのテストをコンテキスト名を含んだクラスに移動します。

1
2
3
4
5
6
class RecordTest
  class AttributesTest
    def test_attributes; end
    def test_recursive_attributes; end
  end
end

コンテキスト名を含んだクラスは移動前のクラスを継承します。

1
2
3
4
5
6
class RecordTest
  class AttributesTest < self
    def test_attributes; end
    def test_recursive_attributes; end
  end
end

最後に、テスト名からコンテキストを除きます。

1
2
3
4
5
6
class RecordTest
  class AttributesTest < self
    def test_single; end
    def test_recursive; end
  end
end

test_attributesは、attributesというコンテキストでは単一の属性についてテストしているので、ここでは新しくtest_singleとしました。 コンテキストがattributesのテストに関しても、attributesというコンテキストが何度も現れる問題が解決されています。 グループ分けをした後のRecordテストは次のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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を使って書くことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
つづき: 2013-01-08
タグ: Ruby | テスト
2012-04-25

デバッグしやすいHTMLのテストの書き方

注意: 長いです。

一言まとめ: withinとtest-unit-capybaraを使ってHTMLのテストを書くと問題を見つけやすくなる。あわせて読みたい: デバッグしやすいassert_equalの書き方

HTMLに対するテストに限らず、開発を進めていく中でテストが失敗する状況になることは日常的にあることです。HTMLの場合は、入力フォームのラベルを変更したり、項目を追加したら既存のテストが失敗するようになるでしょう。そのとき、どのようにテストを書いていれば原因を素早く見つけられるのかを説明します。ポイントは「注目しているノードを明示すること」です。

HTMLテストのライブラリ

さて、Rubyで処理結果のHTMLをテストするときにはどんなライブラリを使っていますか?The Ruby ToolboxにあるBrowser testingカテゴリを見てみると、Capybaraが最も使われていて、次にWebratが広く使われているようです。どちらも同様の機能が揃っているため、特別な使い方をしないならどちらを使っても困ることはないでしょう。もし、今から使いはじめるならCapybaraの方がよいでしょう。これは、Capybaraの方がより活発に開発されているためです。

ここでは、CapybaraでHTMLのテストを書いたときを例にして、どのようにテストを書けば失敗した時に原因をすぐに見つけられるようになるかを説明します。考え方は他のツールでも応用できますし、test-unitやRSpecなどのテスティングフレームワークにも依存しません。そのため、ここでは、もっともRubyらしく書けるテスティングフレームワークであるtest-unit 2を使います。

よくある書き方

まずは一般的なCapybaraでのHTMLのテストがどのように書けるのかを確認します。

まず、以下のようなRackアプリケーションがあるとします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyRackApplication
  def call(env)
    html = <<-HTML
<html>
  <head>
    <title>Welcome! - my site</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <p>No navigation.</p>
    </div>
  </body>
</html>
HTML
   [200, {"Content-Type" => "text/html"}, [html]]
  end
end

はじめにCapybaraでテストを書ける状態にします。include Capybara::DSLCapybara.app=がポイントです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyRackApplication
  # ...
end

gem "test-unit"
require "test/unit"
require "capybara/dsl"

class TestMyRackApplication < Test::Unit::TestCase
  include Capybara::DSL

  def setup
    Capybara.app = MyRackApplication.new
  end
end

それでは、まずは期待した見出しが返ってきているかを確認してみましょう。

1
2
3
4
5
6
7
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_heading
    visit("/")
    assert_equal("Welcome!", find("h1").text)
  end
end

実行するとテストがパスするので正しい値が返ってきていることを確認できます。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
.

Finished in 0.427469388 seconds.

1 tests, 1 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

2.34 tests/s, 2.34 assertions/s

ヘッダー部分も確認しましょう。テスト対象のHTMLの一部も再掲します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyRackApplication
  def call(env)
    html = <<-HTML
<html>
  <!-- ... -->
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <p>No navigation.</p>
    </div>
  </body>
</html>
HTML
   [200, {"Content-Type" => "text/html"}, [html]]
  end
end
# ...
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    assert_equal("No navigation.", find(".header p").text)
  end
end

これもパスします。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
..

Finished in 0.419134417 seconds.

2 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

4.77 tests/s, 4.77 assertions/s

雰囲気はわかりましたね。

HTMLを変更

それでは、HTMLを少し変更してナビゲーション用のリンクをいれましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyRackApplication
  def call(env)
    html = <<-HTML
<html>
  <!-- ... -->
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <ul>
        <li><a href="/">Top</a></li>
      </ul>
    </div>
  </body>
</html>
HTML
   [200, {"Content-Type" => "text/html"}, [html]]
  end
end

HTMLの構造が変わったのでテストは失敗します。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
E
===============================================================================
Error:
test_header(TestMyRackApplication):
Capybara::ElementNotFound: Unable to find css ".header p"
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/finders.rb:154:in `raise_find_error'
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/finders.rb:27:in `block in find'
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/base.rb:46:in `wait_until'
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/finders.rb:27:in `find'
    (eval):2:in `find'
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/dsl.rb:161:in `find'
    test-capybara.rb:40:in `test_header'
===============================================================================
.

Finished in 0.431328598 seconds.

2 tests, 1 assertions, 0 failures, 1 errors, 0 pendings, 0 omissions, 0 notifications
50% passed

4.64 tests/s, 2.32 assertions/s

さて、ここでどうしますか?よく使われる方法は以下のような方法ではないでしょうか。

  • puts sourceを埋め込んでHTMLを確認する。
  • assert_equalのメッセージにsourceを指定する。
  • save_and_open_browserでブラウザで確認する。

それでは、それぞれの方法についてみていきましょう。

puts sourceを埋め込む

include Capybara::DSLするとsourceメソッドが追加され、アプリケーションが返した生のHTMLを確認することができます。

1
2
3
4
5
6
7
8
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    puts source
    assert_equal("No navigation.", find(".header p").text)
  end
end

テストを実行するとHTMLが出力されます。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
<html>
  <head>
    <title>Welcome! - my site</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <ul>
        <li><a href="/">Top</a></li>
      </ul>
    </div>
  </body>
</html>
E
===============================================================================
Error:
test_header(TestMyRackApplication):
Capybara::ElementNotFound: Unable to find css ".header p"
...

出力されたHTMLと「Unable to find css ".header p"」というエラーメッセージと目CSSセレクタを活用して、どうしてCSSセレクタ".header p"がマッチしなくなったのかを考えます。

この方法はテスト自体を書き換える必要があるため、テストが失敗したらテストを修正してもう一度実行し直さなければいけません。テストがパスするようになったら変更を元に戻すことも忘れてはいけません。

assert_equalのメッセージにsourceを指定

assert_equalに限らずアサーションには失敗時に表示する追加のメッセージを指定できます。この方法では、失敗したときのみHTMLが表示されるため「失敗したときだけテストを変更する」といったことをする必要はありません。

1
2
3
4
5
6
7
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    assert_equal("No navigation.", find(".header p").text, source)
  end
end

実行してみます。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
E
===============================================================================
Error:
test_header(TestMyRackApplication):
Capybara::ElementNotFound: Unable to find css ".header p"
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/finders.rb:154:in `raise_find_error'
    ...
===============================================================================
...

おや、エラーメッセージの中にHTMLが出力されていませんね。もう一度assert_equalを確認してみましょう。

1
assert_equal("No navigation.", find(".header p").text, source)

問題なさそうに見えますが、どうして出力されていないのでしょうか。

それは、assert_equalが呼び出される前にfindの中で例外が発生しているからです。Capybaraでは要素が見つけられなかったときはCapybara::ElementNotFound例外を投げるのでassert_equalは呼び出されなかったのです。

これを回避するためには以下のようにfindではなくhas_selector?で事前にCSSセレクタがマッチするかを確認する必要があります。

1
2
3
4
5
6
7
8
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    assert_true(has_selector?(".header p"), source)
    assert_equal("No navigation.", find(".header p").text)
  end
end

これを実行するとHTMLが出力されます。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
F
===============================================================================
Failure:
test_header(TestMyRackApplication) [test-capybara.rb:40]:
<html>
  <head>
    <title>Welcome! - my site</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <ul>
        <li><a href="/">Top</a></li>
      </ul>
    </div>
  </body>
</html>
.
<true> expected but was
<false>
===============================================================================
...

しかし、今度は指定したCSSセレクタが出力されません。

そもそも、findする前に事前に存在を確認するようなテストの書き方になってしまうのでは、せっかくのCapybaraのすっきりした記法を活かせていないと言えるでしょう。

ちなみに、RSpecでは以下のように書くことになります。こちらもHTMLとCSSセレクタを同時に出力してくれないのであまりうれしくありません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
require "capybara/rspec"

class MyRackApplication
  def call(env)
    html = <<-HTML
<html>
  <head>
    <title>Welcome! - my site</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <ul>
        <li><a href="/">Top</a></li>
      </ul>
    </div>
  </body>
</html>
HTML
   [200, {"Content-Type" => "text/html"}, [html]]
  end
end

Capybara.app = MyRackApplication.new

describe MyRackApplication, :type => :request do
  it "should have header content" do
    visit("/")
    page.should have_selector(".header p")
    find(".header p").text.should == "No navigation."
  end
end

実行すると以下のようにCSSセレクタのみが出力されます。

% rspec capybara_spec.rb
F

Failures:

  1) MyRackApplication should have header content
     Failure/Error: page.should have_selector(".header p")
       expected css ".header p" to return something
     # ./capybara_spec.rb:29:in `block (2 levels) in <top (required)>'

Finished in 0.60582 seconds
1 example, 1 failure

Failed examples:

rspec ./capybara_spec.rb:27 # MyRackApplication should have header content

ということで、この方法は「失敗したときだけ必要な情報を出力して原因を素早く見つけたい」という期待する結果を実現できません。メッセージの中にsourceとCSSセレクタを指定すれば実現できなくもありませんが、テストが書きづらくなってしまうため割にあいません。

save_and_open_browserでブラウザで確認

include Capybara::DSLするとsave_and_open_browserメソッドが追加され、アプリケーションが返した生のHTMLをブラウザで開いて確認することができます*1

1
2
3
4
5
6
7
8
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    save_and_open_page
    assert_equal("No navigation.", find(".header p").text)
  end
end

ブラウザでHTMLを確認することによりFirebugなどHTMLの構造を視覚的に確認することができます。確認してどうしてCSSセレクタ".header p"がマッチしなくなったのかを考えます。

この方法もテスト自体を書き換える必要があるため、テストが失敗したらテストを修正してもう一度実行し直さなければいけません。テストがパスするようになったら変更を元に戻すことも忘れてはいけません。元に戻さないままコミットしてしまうと、他の開発者の環境でもテストを実行するたびにブラウザが起動してしまいます。

どの方法がよいか

ブラウザで確認する方法が視覚的で一番わかりやすいですが、ページの内容が増えてくるとページ全体から問題の箇所を素早く探すのは大変です。一方、コンソールにHTMLを出力する方法は視覚的ではありませんが、HTMLをテキストとして検索することができるところは便利です。しかし、やはりページ全体から問題の箇所を素早く探すのは大変です。

小規模のWebアプリケーションでもそこそこのHTMLになり、どの方法でもページ全体から問題の箇所を素早く探すのは大変です。ということで、どの方法も今一歩と言えます。

ページ全体ではなくする

どうして問題の箇所を素早く探すのが大変かというと、探索範囲が広いからです。探索範囲が狭くなれば素早く見つけやすくなります。ついでに言うと、テストの書きやすさを損なわずにできるだけ自然に狭くしたいという希望もあります。

ところで、Capybaraにはwithinというメソッドがあることを知っていますか?これを使うと検索範囲を限定できます。例えば、以下は同じ意味になります。

1
2
3
4
5
6
# これまでの書き方
find(".header p").text
# withinを使った書き方
within(".header") do
  find("p").text
end

withinを使ってテストを書きなおしてみましょう。

1
2
3
4
5
6
7
8
9
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    within(".header") do
      assert_equal("No navigation.", find("p").text)
    end
  end
end

テストを実行するとfindのところで失敗するのですが、この時点では".header"はマッチすることがわかっています。つまり、findはHTML全体ではなく、以下のHTML断片内で"p"がマッチすることを期待しています。

1
2
3
4
5
<div class="header">
  <ul>
    <li><a href="/">Top</a></li>
  </ul>
</div>

このくらいの量であればコンソールに出力されても解析しやすいでしょう。少なくとも「<p>はないが<ul>はある」ということはすぐにわかります。このヒントがあれば原因を特定するのにだいぶ役立つはずです。

では、withinのブロック内でマッチしなかった場合に現在マッチしたノードのHTMLを出力するにはどうしたらよいでしょうか。これを実現できると、失敗した時だけ必要最低限の情報を得られて素早く原因を見つけられそうです。しかもCapybaraの自然な使い方です。

test-unit-capybara

test-unit 2でCapybaraを便利に使うためのtest-unit-capybaraというライブラリがあります。これを使えば、これまで通りCapybaraの作法で書くだけで必要な情報を過不足なく表示してくれます。以下のようにrequire "test/unit/capybara"とするだけでtest-unit-capybaraを使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gem "test-unit"
require "test/unit/capybara"
# require "test/unit"
# require "capybara/dsl"
# ...
class TestMyRackApplication < Test::Unit::TestCase
  include Capybara::DSL

  def setup
    Capybara.app = MyRackApplication.new
  end

  # ...

  def test_header
    visit("/")
    within(".header") do
      assert_equal("No navigation.", find("p").text)
    end
  end
end

実行すると以下のようになります。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
F
===============================================================================
Failure:
test_header(TestMyRackApplication)
    [/var/lib/gems/1.9.1/gems/test-unit-capybara-1.0.1/lib/test/unit/capybara.rb:77:in `raise_find_error_for_test_unit'
...
     test-capybara.rb:40:in `test_header']:
<"p">(:css) expected to find a element in
<<div class="header">
      <ul>
<li><a href="/">Top</a></li>
      </ul>
</div>>
===============================================================================
...

探索範囲であるHTML断片<div class="header">...</div>とCSSセレクタ"p"が表示されています。これがわかれば素早く原因を見つけられますね。

最近のCapybaraはfindメソッドでノードを見つけられなかったときの挙動をカスタマイズできます*2。test-unit-capybaraはその機能を使って必要な情報を収集して表示しています。同様のことは他のテスティングフレームワークでも実現できるでしょう。

まとめ

現状のテストの書き方・ツールではHTMLに対するテストが失敗したときの原因を素早く見つけることが困難であることを示しました。また、解決方法としてwithinを使って「注目しているノードを明示」し、テストツールとしてtest-unit-capybaraを使う方法を紹介しました。この方法は他のテスティングフレームワークでも実現できる一般的な方法です。

インターフェイスの改良や文言の変更などでHTMLのテストが失敗することはよくあることです。そんなときもすぐにどこが変わったかに気付けるようなテストだと変更を嫌がらずによいものを作ることに専念できますよね

あわせて読みたい: デバッグしやすいassert_equalの書き方

おまけ: assert_match問題

ここで取り上げた問題は「問題があるだろう範囲が広すぎて問題を発見することが困難になる」ことが原因です。同様の問題はtest-unitのassert_match、RSpecのshould matchにもあります。

以下はUser-Agentが任意のバージョンのbingbotであることをテストしています。このテストは失敗するのですが、どうして失敗するかすぐにわかるでしょうか?

1
2
3
4
5
6
7
def assert_bingbot(user_agent)
  assert_match(/;\s+bingbot\/[\d]+;/, user_agent)
end

def test_bot
  assert_bingbot("Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)")
end

正規表現は少し間違えて書いてしまうとどこが間違えているのか見つけることが大変です。これを防ぐために、assert_matchではなく、以下のようにマッチ対象を正規化してからassert_equalで比較することをオススメします。

1
2
3
4
5
6
7
8
def assert_bingbot(user_agent)
  assert_equal("Mozilla/5.0 (compatible; bingbot/XXX; +http://www.bing.com/bingbot.htm)",
               user_agent.gsub(/bingbot\/[\d]+/, "bingbot/XXX"))
end

def test_bot
  assert_bingbot("Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)")
end

assert_equalだと以下のようにdiffがでるので何が悪かったのかがすぐにわかります。

===============================================================================
Failure:
test_bot(TestMyRackApplication)
    [test-capybara.rb:50:in `assert_bingbot'
     test-capybara.rb:55:in `test_bot']:
<"Mozilla/5.0 (compatible; bingbot/XXX; +http://www.bing.com/bingbot.htm)"> expected but was
<"Mozilla/5.0 (compatible; bingbot/XXX.0; +http://www.bing.com/bingbot.htm)">

diff:
? Mozilla/5.0 (compatible; bingbot/XXX.0; +http://www.bing.com/bingbot.htm)
===============================================================================

バージョン番号を検出しようとしていた[\d]+が「.」を考慮していないことが原因ですね。

*1  別途launchy gemが必要なことに注意。

*2  raise_find_errorメソッドをオーバーライドする

つづき: 2013-01-08
タグ: Ruby | テスト
2012-01-18

デバッグしやすいassert_equalの書き方

デバッグしやすいassert_equalの書き方とデバッグしにくいassert_equalの書き方があるのは知っていますか?*1

デバッグしやすいassert_equalの書き方を2パターン紹介します。

まとめたassert_equal

まず、1つ目のよくみるデバッグしにくいassert_equalの書き方です。

1
2
3
4
5
def test_parse
  assert_equal(29, parse_integer("29"))   # (1)
  assert_equal(29, parse_integer("+29"))  # (2)
  assert_equal(-29, parse_integer("-29")) # (3)
end

これがデバッグしにくいのは、(1)が失敗したら(2)、(3)が実行されないからです。すべてのassert_equalが実行されて、どのassert_equalが失敗したかを確認することでバグの傾向を見つけやすくなります。

例えば…(1)だけが失敗するなら符号が無い場合の処理が怪しいでしょう。(3)だけが失敗するなら、マイナス記号に対応していないのかもしれません。全部失敗するなら根本的におかしいのでしょう。

このように、どのassert_equalが失敗したかがデバッグのヒントになります。よって、↑のような書き方はデバッグがしにくいassert_equalの書き方といえます。

デバッグしやすく書くと、例えば、こうなります。

1
2
3
4
5
6
7
8
9
10
11
def test_parse_no_sign
  assert_equal(29, parse_integer("29"))
end

def test_parse_plus_sign
  assert_equal(29, parse_integer("+29"))
end

def test_parse_mius_sign
  assert_equal(-29, parse_integer("-29"))
end

まとめないassert_equal

続いて、1つ目のよくみるデバッグしにくいassert_equalの書き方です。

1
2
3
4
5
def test_parse
  bob = MyJSONParser.parse('{"name": "bob", "age": 29}')
  assert_equal("bob", bob["name"])
  assert_equal(29, bob["age"])
end

1つ目の書き方のように複数のassert_equalが書いてあります。1つ目の書き方と同じように直すならこうなります。

1
2
3
4
5
6
7
8
9
def test_parse_string_value
  bob = MyJSONParser.parse('{"name": "bob", "age": 29}')
  assert_equal("bob", bob["name"])
end

def test_parse_numeric_value
  bob = MyJSONParser.parse('{"name": "bob", "age": 29}')
  assert_equal(29, bob["age"])
end

でも、本当にこれでよいでしょうか。この書き方では値の型だけを注目しています。値の型に注目するならば以下のように書いた方がよいでしょう。こうした方が余計なものがなくなり、より注目できています。

1
2
3
4
5
6
7
8
9
def test_parse_string_value
  bob = MyJSONParser.parse('{"name": "bob"}')
  assert_equal("bob", bob["name"])
end

def test_parse_numeric_value
  anonymous = MyJSONParser.parse('{"age": 29}')
  assert_equal(29, anonymous["age"])
end

もし、最初のコードが「複数のキーと値をパースできること」を確認したい場合はassert_equalを複数のテストに分割するのではなく、複数のassert_equalを1つのassert_equalにまとめるべきです。

1
2
3
4
5
def test_parse
  bob = MyJSONParser.parse('{"name": "bob", "age": 29}')
  assert_equal({"name" => "bob", "age" => 29},
               bob)
end

1つのassert_equalにまとめると、失敗する場合でも、一部の比較だけが実行されるのではなく、すべての比較が実行されます。そのため、"name"はパースに成功しているけど"age"は失敗している、というように、失敗の傾向がわかります。失敗の傾向がわかるとデバッグしやすくなるというのは前述の通りです。

おまけ: Hashのassert_equal結果が見づらい

ところで、Hashのassert_equalの結果は見づらいですよね。これは、RSpecでも一緒です。

1
{"age" => 30, "name" => "bob"}.should == {"name" => "bob", "age" => 29}

この結果は以下のようになります。

expected: {"name"=>"bob", "age"=>29}
     got: {"age"=>30, "name"=>"bob"} (using ==)
Diff:
@@ -1,2 +1,2 @@
-{"name"=>"bob", "age"=>29}
+{"age"=>30, "name"=>"bob"}

一見しただけではどこが違うのかわかりません。

しかし、最新のtest-unit 2は一味違います。

1
2
assert_equal({"name" => "bob", "age" => 29},
             {"age" => 30, "name" => "bob"})

この結果は以下のようになります。

<{"age"=>29, "name"=>"bob"}> expected but was
<{"age"=>30, "name"=>"bob"}>

diff:
? {"age"=>29, "name"=>"bob"}
?         30

違いがわかりやすいですね。

まとめ

デバッグしやすいassert_equalの書き方を2パターン紹介しました。今度から、assert_equalを書くときは、単に数を増やしたりするのではなく、どうすれば有用なassert_equalになるかを考えてみてください。assert_equalが失敗したときにいかにデバッグしやすいかがヒントになるはずです。

*1  assert_equalではなくて、should ==と読み替えてもよい。

タグ: Ruby | テスト
2011-02-28

TDDきのたん

今日は年に一度の肉の日だからか、いろいろなソフトウェアがリリースされていますね。

このうち、Cutter 1.1.6について紹介します。

Cutterとは

CutterはC/C++用の単体テストフレームワークです。スクリプト言語の単体テストフレームワークのように簡単にテストを書けること、テストが失敗した時にデバッグしやすいことを重視しています。どちらも「テストが苦痛」にならないために大事なことです。

Cutter 1.1.6ではテストをより頻繁に実行しやすくするための機能を強化しました。それがTDDきのたんのサポートです。

TDDきのたんとは

TDDきのたんとはMayu & Co.さんが描いた色違いのかさのきのこたちです。Cutter 1.1.6を使うとテストを実行するたびに愛らしいTDDきのたんに会うことができます。頻繁にテストを実行したくなりますね。

TDDきのたんのスクリーンショット

テストとグリーンとレッドとフィードバック

TDD(テスト駆動開発)ではテストの結果を色で表すことが多いです。グリーンならパスで、レッドなら失敗です。グリーンになったら気持ちいいよね、レッドは落ち着かないね、早くグリーンにしたいね、そんな風に色を使っています。色を使うことで、テストをすべてパスしている状態をキープしたくなる力を推進しています。

RSpecやCutterなど最近も開発が続いている単体テストフレームワークは結果を表示する時にグリーンやレッドなどの色を使っています。でも、それだけで十分でしょうか。

テストを頻繁に実行するようになると、開発の中で自然にテストを実行するようになります。それはもうlsコマンドを実行するように*1テストを実行します。そうなると、テストの実行中に違う作業をして、合間にテスト結果を見るようになります。そのため、テストの失敗に気付くのが遅れる場合があります。

それを解決するためには、これまで通り端末に色付きの結果を表示させているだけでは不十分ではないでしょうか。

通知

デスクトップ環境では多くのアプリケーションが動いていて、常にすべてのアプリケーションを見ていることはできません。そこで必要なときにアプリケーションから「通知」してきます*2。Linuxや*BSDではDesktop Notification、Mac OS XではGrowlなどを利用します。

テスト結果も「通知」した方がよいのではないでしょうか。

そういうわけで、Cutter 1.1.6では「notify-send」コマンドを使ってTDDきのたん付きの通知を行うようになりました。test-unit 2では2ヶ月くらい前からTDDきのたん付きの通知をサポートしています。test-unit 2で利用する場合はtest-unit-notifyを使います。こちらもTDDきのたんを使っています。

まわりくどかったですね。

まとめ

まわりくどくCutter 1.1.6の新機能である「テスト結果通知」を紹介しました。単体テストフレームワークに限らず、うるさくない程度に「通知」に対応するとより使いやすくなるかもしれません。

「通知」を使う場合は画像も入れることをおすすめします。画像を入れるとよりピンときます。例えば、Mayu & Co.さんのTDDきのたんは愛らしくて何度も会いたくなりますよね。それでは、テストを実行して会いにいきましょう。

*1  慣用句です。実際にlsコマンドを実行しまくっている人はあまりシェルを使いこなしていないのでしょう。

*2  デスクトップだけではなくスマートフォンなどの携帯端末でも「通知」してきます。

タグ: Cutter | Ruby | テスト
2011-02-09

test-unit 2.0.7とCutter 1.1.1をリリース

先日、Ruby用のxUnit系テスティングフレームワークtest-unit 2.0.7とC・C++用のxUnit系テスティングフレームワークCutter 1.1.1がリリースされました。

どちらも、テストの書きやすさ(テストをキレイに書けるとテストを保守しやすい)だけではなく、テストが失敗した時に「できるだけ素早く問題の原因にたどり着ける」ことも重視しています。

Rails/Rack界隈ではCucumberWebratcapybaraなどを使って、「"ログイン"ボタンをクリックする」とか「click_link("ログイン")」などと、直感的にテストを書けるようになっています。では、テストが失敗したときの結果はどのように表示されるでしょうか。HTML全体やテキスト全体が表示されて、「"ログイン"というボタン(リンク)はなかったよ」と言われたらどうでしょう。あなたのコードはどこが悪かったのでしょう。

そういうときに、失敗結果を見て、すぐに「あぁ、ここが悪いかも!」と作業を進めていけるようなテスティングフレームワークにしたいものです。開発はデバッグの連続なのですから、よりスムーズにデバッグ作業を進める手助けとなるツールを使って開発したいですよね。

test-unitやCutterはWebアプリケーション用に特別なサポート機能は提供していないので上記のようなことをうまい具合に解決できるわけではないのですが、ライブラリのテストでは上記のようなことをうまい具合に解決する機能を提供しています。

Cutterのインストール方法がより簡単に

一応、リリースで変わったことを少し書いておきます。

機能面でもよくなっているのですが、インストールまわりだけにしておきます。

Cutterプロジェクトでは、これまでDebian, Ubuntu用のapt-lineとMacPortsのPortfileを提供していましたが、今回のリリースから、FedoraのYumリポジトリも提供するようにしました。以下でインストールできます。yumの管理下に入るのでアップデートも簡単ですね。

% sudo rpm -Uvh http://cutter.sourceforge.net/fedora/cutter-repository-1.0.0-0.noarch.rpm
% sudo yum install cutter

test-unitの方は変わらずgemをサポートしているので、こちらもインストール・アップデートが簡単ですね。

書きやすさだけではなくデバッグしやすさも重視したテスティングフレームワークに興味のある方は使ってみてはいかがでしょうか。

タグ: テスト | Ruby | Cutter
2010-03-11

処理待ちをより簡単に行えるようになったUxU 0.7.6をリリースしました

2010年1月29日付で、テスティングフレームワークUxUのバージョン0.7.6をリリースしました。

新機能のutils.wait()について

今回のアップデートでの目玉となる新機能は、非同期な機能のテストをより簡単に記述できるようにするヘルパーメソッドであるutils.wait()です。これは、以下のように利用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function testSendRequest() {
  myFeature.sendRequest();
  utils.wait(1000); // 1000ミリ秒=1秒待つ
  assert.equals('OK', myFeature.response);
}

function testLoad() {
  var loaded = { value : false };
  content.addEventListener('load', function() {
    content.removeEventListener('load', arguments.callee, false);
    loaded.valeu = true;
  }, false);
  myFeature.load();
  utils.wait(loaded); // valueがtrueになるまで待つ
  assert.equals('OK', content.document.body.textContent);
}

また、utils.wait()は関数の中でも利用できます。

1
2
3
4
5
6
7
8
9
10
function testSendRequest() {
  function assertSend(aExpected, aURI) {
    myFeature.sendRequest(aURI);
    utils.wait(1000);
    assert.equals(aExpected, myFeature.response);
  }
  assertSend('OK', '...');
  assertSend('NG', '...');
  assertSend('?', '...');
}

utils.wait()が受け取れる値は、これまでの処理待ち機能で yieldに渡されていた値と同じです。詳しくは処理待ち機能の利用方法をご覧下さい。大抵の場合、yield Do(...);と書かれていた箇所は、utils.wait(...);へ書き換えることができます。

ただし、このヘルパーメソッドはFirefox 3以降やThunderbird 3以降など、Gecko 1.9系の環境でしか利用できません。Thunderbird 2などのGecko 1.8系の環境ではエラーとなりますのでご注意下さい。(それらの環境でもテストの中で処理待ちを行いたい場合は、従来通りyieldを使用して下さい。)

これまでの処理待ち機能の特徴と欠点

これまでUxUでは、「機能を実行した後、N秒間待ってから、機能が期待通りに働いたかどうかを検証する」「初期化処理で、ページの読み込みの完了を待ってから次に進む」といった処理待ちを実現する際は、JavaScript 1.7以降で導入されたyieldを使う仕様となっていました。

yieldを含む関数はジェネレータとなり、関数の戻り値をイテレータとして利用できるようになります。この時ジェネレータの内側から見ると、「yieldが出現する度に処理が一時停止する」という風に考えることができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function gen() {
  alert('step1');
  yield 'step1 done';
  alert('step2');
  yield 'step2 done';
  alert('step3');
}
var iterator = gen(); // この時点ではまだ関数の内容は評価されない
var state;
state = iterator.next(); // 'step1' が表示される
alert(state);            // 'step1 done' が表示される
state = iterator.next(); // 'step2' が表示される
alert(state);            // 'step2 done' が表示される
try {
  state = iterator.next(); // alert('step3'); が実行される
} catch(e if e instanceof StopIteration) {
  // 次のyieldが見つからないので、StopIteration例外が投げられる
}

UxUに従来からある処理待ち機能は、この考え方を推し進めて作られています。テスト関数の中にyieldがある場合(つまり、関数の戻り値がイテレータとなる場合)は、フレームワーク側で自動的にイテレーションを行い、yieldに渡された値をその都度受け取って、次にイテレーションを行うまでの待ち条件として利用しています。例えば、数値が渡された場合はその値の分の時間だけ待った後で次にイテレーションを行う、といった具合です。

このやり方の欠点は、yieldを含む関数から任意の戻り値を返すことができないという点です。

1
2
3
4
5
6
7
8
9
10
11
function gen() {
  alert('step1');
  yield 'step1 done';
  alert('step2');
  return 'complete';
}
try {
  var iterator = gen();
} catch(e) {
  alert(e); // TypeError: generator function gen returns a value
}

returnを書くと、関数の実行時にエラーになってしまいます。どうしても何らかの値を取り出したい場合は、値を取り出すためのスロットとなるオブジェクトを引数として渡すなどの工夫が必要になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function gen(aResult) {
  alert('step1');
  yield 'step1 done';
  alert('step2');
  aResult.value = 'complete';
}
var result = {};
var iterator = gen(result);
var state;
state = iterator.next(); // 'step1' が表示される
alert(state);            // 'step1 done' が表示される
try {
  state = iterator.next(); // 'step2' が表示される
} catch(e if e instanceof StopIteration) {
  alert(result.value); // 'complete' が表示される
}

また、ジェネレータは実行してもその段階では関数の内容が評価されないという点にも注意が必要です。例えば以下のようなテストは、期待通りには動いてくれません。

1
2
3
4
5
6
7
8
9
10
function testSendRequest() {
  function assertSend(aExpected, aURI) {
    myFeature.sendRequest(aURI);
    yield 1000;
    assert.equals(aExpected, myFeature.response);
  }
  assertSend('OK', '...');
  assertSend('NG', '...');
  assertSend('?', '...');
}

この例では、assertSend()を実行したことで戻り値としてイテレータが返されているものの、そのイテレータに対するイテレーションが一切行われていないため、リクエストも行われなければアサーションも行われないということになってしまっています。これは以下のように、返されたイテレータをそのままフレームワークに引き渡して、フレームワーク側でイテレーションを行わせる必要があります。

1
2
3
4
5
6
7
8
9
10
function testSendRequest() {
  function assertSend(aExpected, aURI) {
    myFeature.sendRequest(aURI);
    yield 1000;
    assert.equals(aExpected, myFeature.response);
  }
  yield assertSend('OK', '...');
  yield assertSend('NG', '...');
  yield assertSend('?', '...');
}

また、このままではジェネレータの中で発生した例外のスタックトレースを辿れないという問題もあります。スタックを繋げるためには、ヘルパーメソッドのDo()を使ってyield Do( assertSend('OK', '...') )のように書かなければなりません。

utils.wait()を使う場合、これらのことを考慮する必要はありません。冒頭のサンプルコードのように、素直に書けば素直に動作してくれます。

スレッド機能を使った処理待ち

utils.wait()がどのように実装されているかについても解説しておきます。

このメソッドの内部では、Gecko 1.9から実装されたスレッド関連の機能を利用しています。

1
2
3
4
window.setTimeout(function() {
  alert('before');
}, 0);
alert('after');

JavaScriptは基本的にシングルスレッドで動作するため、このようにタイマーを設定すると、その処理はキューに溜められた状態となります。その上で、メインの処理が最後まで終わった後でやっとキューの内容が処理され始めるため、この例であれば「after」「before」の順でメッセージが表示されることになります。

1
2
3
4
5
6
7
8
9
10
11
12
var finished = false;
window.setTimeout(function() {
  alert('before');
  finished = true;
}, 0);
var thread = Cc['@mozilla.org/thread-manager;1']
              .getService()
              .mainThread;
while (!finished) {
  thread.processNextEvent(true);
}
alert('after');

Gecko 1.9以降のスレッド機能を使うと、メインの処理を一旦中断して先にキューに溜められた処理の方を実行し、その後改めてメインの処理に戻るということができます。実際に、こちらの例では「before」「after」の順でメッセージが表示されます。UxU 0.7.6ではこれを応用して、任意の条件が満たされるまでthread.processNextEvent(true);でメインの処理を停止し続けることによって、処理待ちを実現しています。

なお、HTML5にもWeb Workersというスレッド関係の機能がありますが、こちらは別スレッドでスクリプトを動作させる機能しか持っていないため、上記のようなことは残念ながらできません。

まとめ

UxU 0.7.6からは、utils.wait()を使ってより簡単に処理待ちを行えるようになりました。Firefox 3以降やThunderbird 3以降専用にアドオンを開発する際には、是非利用してみて下さい。

つづき: 2010-12-29
タグ: UxU | テスト | Mozilla
2010-01-29

FSIJ月例会の資料公開

先日開催されたFSIJ月例会でテストとテスティングフレームワークについて話しました。声をかけてくれた上野さん、ありがとうございます。

テスティングフレームワークに必要なもの - 書きやすさとデバッグのしやすさ

内容

これまでの経験から感じていることをまとめたものになっています。どうにかしたいけど、まだよいアイディアがなくて悩んでいる、といったものも含まれています。例えば、違いをわかりやすく示すことはいろいろ工夫できるけど、なかったことをわかりやすく示すことは難しいといったことです。

最後まで話しつづけるのではなく、随時ディスカッションをはさむ形で進みました。参加者のみなさんからもたくさんの意見がでてとても有意義でした。

GUIやDTP、ネットワーク・ハードウェア周辺などテストを自動化することが難しい領域のテストはみなさん悩んでいるところでした。今回の会で決定的な解決策がでたわけではないのですが、悩んでいる部分、他の人が持っているよいアイディアを共有できたことは成果と言えます。明日から劇的に変わるというものではありませんが、確実によくなっていけるというものです。

話の中に何度かでていますが、「完璧を求めない」ことが重要です。完璧を求めてテストがストレスになってしまうことよりも、自分たちのペースでよい状態を継続し、よいソフトウェアの開発を続けられるようにすることの方が重要です。このあたりの力の入れ具合、テストが目的ではなくよいソフトウェアを開発することが目的であることを忘れないことで「壁」を越えることができるでしょう。

あわせて読みたい

つづき: 2009-12-21
タグ: テスト
2009-11-27

告知: 2009/11/26のFSIJ月例会はテスティングフレームワークについて

今月末、FSIJ月例会でテスティングフレームワークについて話します。月例会の概要は以下の通りです。都合があう方はぜひお越しください。

テーマ
テスティングフレームワークに必要なもの - 書きやすさとデバッグのしやすさ
日時
11月26日(木)18:30〜20:30
場所
秋葉原ダイビル 11階 大会議室1
参加方法
メールで申し込み

(tokyo-emacs #x02)話したときにお会いした上野さんに声をかけてもらいました。声をかけていただきありがとうございます。

進め方

事前に打ち合わせなどはないようなので、自由な形式でやる予定です。

2時間ありますが、ずっと話し続けるわけではありません*1。こちらから考えていることや技術的なことなどを紹介し、それを話題としてディスカッションを挟みながら進めていきます。テスティングフレームワークやテストについて思っていることがある方はぜひ参加してその考えを聞かせてください。

内容

以下の3部構成にする予定です。ここでいう「テスト」とは実装をする開発者自身が書く「自動化されたテスト」のことです。

  1. テストが当たり前の人になるには
  2. テスティングフレームワークに必要なもの
  3. 各種テスティングフレームワークの実例

今では、「テストは書いた方がよい」というのはみんなわかっていることです。しかし、「テストのないコードは怖くてさわれない」という感覚はそうではありません。それがテストが当たり前の人とそうでない人の差です*2。では、どうすればテストが当たり前になるのか。まずは、それについて話し、ディスカッションします。

次に、テストが当たり前の人が価値のあるテストを継続して書くために、テスティングフレームワークが提供するべきものはなにかを考えます。

最後に、現在のテスティングフレームワークはどうなっているかを扱います。先日、C++用xUnitでのテストの書き方を紹介しましたが、他の言語用のテスティングフレームワークでの書き方や、もしかしたら、どのような実装で実現しているかまで紹介できるかもしれません。

随時、参加者からの意見を受け入れますが、それぞれの話題毎および最後にもまとまったディスカッションの時間をとる予定です。

まとめ

今月のFSIJ月例会でテスティングフレームワークについて話すことになったので、それの告知をしました。参加費は無料なので興味のある方はお気軽に参加してみてください。

日時
11月26日(木)18:30〜20:30
場所
秋葉原ダイビル 11階 大会議室1
参加方法
メールで申し込み

*1  2時間もただ聞いていると飽きますよね。

*2  テストがあることをポジティブに捉えるかテストがないことをネガティブに捉えるかの差と言ってもよいかもしれません。

つづき: 2009-11-27
タグ: テスト
2009-11-17

C++用xUnitでのテストの書き方

注: 長いです。

スクリプト言語でのxUnit実装を使ったことがある方なら、テストを定義するだけでテストが実行されることが当たり前ではないでしょうか。c2.comのWikiによると、これはTest Collectorというそうです。定義したテストを自動的に集めてくる機能のことです。

一般的にTest Collectorの機能は言語が提供するリフレクション機能やメタプログラミング機能を使って実現されます。

例えば、Rubyのtest-unit 2.xでは、リフレクションを使う方法とメタプログラミングを使う方法の両方をサポートしています。リフレクションを使う方法ではObjectSpace.each_object(Class)ですべてのクラスを取得し、その中のTest::Unit::TestCaseのサブクラスを集めます。メタプログラミングを使う方法ではTest::Unit::TestCase.inheritedを定義して、サブクラスが定義された時のフックでそのサブクラスを集めます。

Pythonでもモジュールオブジェクトからモジュール内のオブジェクトにアクセスすることができるので、同様の方法でテストを集めることができます。JavaScriptでも、オブジェクトに定義されているプロパティを列挙することができるので、同様の方法でテストを集めることができます。(JavaScriptとXULで実装されているUxUも同様のことをしています。)

一方、CやC++ではリフレクション機能やクラスをファーストクラスオブジェクトとして扱う機能がないため、自動でテストを集めるためには一工夫必要になります。一昔前のxUnitでは、定義したテストを手動で登録していました。

それでは、C/C++での一工夫の方法として以下の4つをxUnit実装と一緒に紹介します。C++用のxUnitを選択する時の参考にしてください。

  • マクロでごまかす
  • プリコンパイルする
  • マクロでテスト定義と同時に登録する
  • 共有ライブラリから探す

マクロでごまかす

最初はCppUnitのケースです。CppUnitではテストを定義するだけでは、自動でテストを集めてはくれません。しかし、便利マクロを用意して、手動でテストを登録する面倒さを減らしています。

以下はCppUnit Cookbookにあるソースコードをベースにしています。public内のテスト定義とは別にCPPUNIT_TEST_SUITEからCPPUNIT_TEST_SUITE_ENDの間でテストを登録しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// complex-number-test.cpp
#include <cppunit/extensions/HelperMacros.h>

class ComplexNumberTest : public CppUnit::TestFixture  {
  CPPUNIT_TEST_SUITE( ComplexNumberTest );
  CPPUNIT_TEST( testEquality );
  CPPUNIT_TEST( testAddition );
  CPPUNIT_TEST_SUITE_END();

private:
  Complex *m_10_1, *m_1_1, *m_11_2;

public:
  void setUp()
  {
     m_10_1 = new Complex( 10, 1 );
     m_1_1 = new Complex( 1, 1 );
     m_11_2 = new Complex( 11, 2 );
  }

  void tearDown()
  {
    delete m_10_1;
    delete m_1_1;
    delete m_11_2;
  }

  void testEquality()
  {
    CPPUNIT_ASSERT( *m_10_1 == *m_10_1 );
    CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) );
  }

  void testAddition()
  {
    CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 );
  }
};
CPPUNIT_TEST_SUITE_REGISTRATION( ComplexNumberTest );

実行する場合は以下のようなmain関数を定義する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.cpp
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>

int main( int argc, char **argv)
{
  CppUnit::TextUi::TestRunner runner;
  CppUnit::TestFactoryRegistry &registry =
    CppUnit::TestFactoryRegistry::getRegistry();
  runner.addTest( registry.makeTest() );
  bool wasSuccessful = runner.run( "", false );
  return wasSuccessful;
}

上記の二つのファイル(とComplexNumberの実装)を使ってビルドします。

% g++ -o complex-number-test complex-number-test.cpp main.cpp \
    -lcomplex-number -lcppunit

テストを実行するにはビルドしたバイナリを実行します。

% ./complex-number-test
..


OK (2 tests)

マクロを使ってテスト登録を簡単にしている(自動化まではしていない)例としてCppUnitを紹介しました。

CPPUNIT_TEST_SUITEなどの便利マクロを使わない場合は定義したテスト名(上の例ではtestEqualitytestAddition)以外のことも気にしなければいけなくなります。便利マクロを使うと、テスト名だけわかっていればよいので、それに比べるとだいぶテスト作成が楽になっています。

しかし、テストを定義だけして登録し忘れたということを回避することができません。また、テストケース定義とは別にmain関数も定義する必要があり、テスト以外のことにも気を配る必要があることにも注意が必要です。

プリコンパイルする

C++で書かれたテストコードを直接C++コンパイラでコンパイルするのではなく、テストコードに必要なコードを追加したC++ソースコードを生成して、それをコンパイルする方法です。C++コンパイラでのビルドする前に一度変換処理を行えるので、テストコードへの記述が減ることが利点ですが、変換処理を行うのがやや面倒です。自動化されれば気にならなくなるでしょう。

CxxTest

まずはCxxTestのケースです。CxxTestではソースコードを直接ビルドするのではなく、C++のソースコードからテスト登録処理などを付加したC++ソースコードを生成し、それをビルドします。

以下はCxxTest User Guideにあるソースコードをベースにしています。テストの定義だけでテスト登録処理は含まれていません。

1
2
3
4
5
6
7
8
9
10
11
12
// MyTestSuite.h
#include <cxxtest/TestSuite.h>

class MyTestSuite : public CxxTest::TestSuite
{
public:
   void testAddition( void )
   {
      TS_ASSERT( 1 + 1 > 1 );
      TS_ASSERT_EQUALS( 1 + 1, 2 );
   }
};

以下のようにビルドします。

% cxxtestgen --error-printer -o cxxunit-tests.cpp MyTestSuite.h
% g++ -o cxxunit-tests cxxunit-tests.cpp

cxxtestgenMyTestSuite.hにあるテスト定義にテスト登録処理などを加えてcxxunit-tests.cppを生成します。余談ですが、cxxtestgenはPythonスクリプトです。また、CxxUnitはライブラリを提供せず、ヘッダーファイルのみを提供します。

バイナリを実行するとテストが走ります。

% ./cxxunit-tests
Running 1 test.OK!

テスト登録が完全に自動化されているのでCppUnitよりも新規テストの追加が容易です。テストの登録しわすれもありません。ただ、cxxtestgenとC++コンパイラで2回コンパイルする必要があることが少し手間だと言えます。

QTestLib

続いてQtが提供するQTestLibのケースです。QTestではQtが提供するスロットの仕組みを使って、定義されているテストを集めます。スロットがどのように定義されているかをプログラム中から扱うために、QtはC++のソースコードをプリコンパイルしますが、QTestでも同様にプリコンパイルする必要があります*1

以下はQTestLibのチュートリアルにあるソースコードををベースにしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// test-qstring.cpp
#include <QtTest/QTest>

class TestQString: public QObject
{
    Q_OBJECT
private slots:
    void toUpper()
    {
        QString str = "Hello";
        QCOMPARE(str.toUpper(), QString("HELLO"));
    }
};

QTEST_MAIN(TestQString)
#include "test-qstring.moc"

QTestLibでもmain関数を定義しなければいけませんが、QTEST_MAINという便利マクロが用意されています。

以下のようにビルドします。

% mkdir test-qstring
% mv test-qstring.cpp test-qstring
% cd test-qstring
% qmake -project "QT += testlib"
% qmake
% make

バイナリを実行するとテストが走ります。

% ./test-qstring
********* Start testing of TestQString *********
Config: Using QTest library 4.5.3, Qt 4.5.3
PASS   : TestQString::initTestCase()
PASS   : TestQString::toUpper()
PASS   : TestQString::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped
********* Finished testing of TestQString *********

このようにQTestLibではテスト登録のために必要なコードはQTEST_MAINでクラスを指定している部分だけです。個々のテストは指定する必要がありません。

メタオブジェクト情報を生成すること、また、それを読み込んでいる#include "test-qstring.moc"のところはQTestLib独自のことではなく、Qt全般のことです。そのため、Qtを利用している場合は追加で必要な作業とはいえないでしょう。つまり、QTestLibのテストを集める方法は完全には自動化されていませんが、Qt開発者にはそれほど負担もかからず自然に書けるようになっている使いやすいAPIといえます。一方、Qt開発者でない場合は、面倒に見えるでしょう。

マクロでテスト定義と同時に登録する

CppUnitでもマクロでテストを登録していますが、それをもう一歩進めたのがこの方法です。CppUnitでは、テスト定義は通常の関数定義でしたが、この方法ではそこでマクロを使い、テスト定義と同時にテストを登録します。

Google Test

まずは、Google Testです。Google Testではテスト定義時にTESTマクロを使います。以下はGoogleTestSamplesにあるソースコードをベースにしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// test-factorial.cpp
#include <gtest/gtest.h>

int Factorial(int n) {
  int result = 1;
  for (int i = 1; i <= n; i++) {
    result *= i;
  }

  return result;
}

TEST(FactorialTest, Negative) {
  EXPECT_EQ(1, Factorial(-5));
  EXPECT_EQ(1, Factorial(-1));
  EXPECT_TRUE(Factorial(-10) > 0);
}

int main(int argc, char **argv) {
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

プリコンパイル方式でもテストの登録忘れはありませんが、この方法でも登録を忘れることがありません。テスト定義の方法が通常の関数定義とは異なる書式になることに慣れることができるのであれば、この方式で負担が少なくテストを書けるようになるでしょう。

以下のようにビルドします。

% g++ -o test-factorial test-factorial.cpp -lgtest

バイナリを実行するとテストが走ります。

% ./test-factorial
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from FactorialTest
[ RUN      ] FactorialTest.1
[       OK ] FactorialTest.1 (0 ms)
[----------] 1 test from FactorialTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 1 test.

上記の例では触れていませんが、フィクスチャ(setup/teardown)を使う場合は、テストクラス名を揃える必要があるなど、同じグループのテストを作る場合は重複する部分がでてしまいます。例えば、QTestLibのようにクラス内にテストを定義する方法では以下のようになります。

1
2
3
4
5
6
7
8
9
class MyTest
{
   void setup() {...}
   void teardown() {...}

   void test1() {...}
   void test2() {...}
   void test3() {...}
}

一方、Google Testの場合は、スコープが使えず、以下のようにクラス名を複数回書く必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FooTest : public testing::Test {
 protected:
  virtual void SetUp() { b_.AddElement(3); }

  Foo a_;
  Foo b_;
};

TEST_F(FooTest, InitializesCorrectly) {
  EXPECT_TRUE(a_.StatusIsOK());
}

TEST_F(FooTest, ReturnsElementCountCorrectly) {
  EXPECT_EQ(0, a_.size());
  EXPECT_EQ(1, b_.size());
}

マクロを使っている場合は、間違ったテストクラス名を指定するなどコンパイルエラーになったときに意味の分からないエラーメッセージを目にすることがあるというのも注意しなければいけないポイントです。エラーメッセージを使えないと問題を発見することが難しくなります。

Boost Test Library

次に、Boost Test Libraryです。やり方はGoogle Testとだいたい同じで、Boost Test LibraryではBOOST_AUTO_TEST_CASEを使います。

1
2
3
4
5
6
7
8
9
10
11
// test-add.cpp
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE AddTest
#include <boost/test/unit_test.hpp>

int add( int i, int j ) { return i+j; }

BOOST_AUTO_TEST_CASE( add_test )
{
    BOOST_CHECK_EQUAL( add( 2,3 ), 5);
}

最初にBOOST_TEST_DYN_LINKBOOST_TEST_MODULEを定義しておくと、Boost Test Libraryではmain関数を定義する必要はありません。

以下のようにビルドします。

% g++ -o test-add test-add.cpp -lboost_unit_test_framework

バイナリを実行するとテストが走ります。

% ./test-add
Running 1 test case...

*** No errors detected

Google Testと同じくマクロが気にならない場合やBoostに慣れている場合はテストが書きやすいでしょう。

共有ライブラリから探す

マクロを利用する方法は言語の構文を工夫してテストを集めています。プリコンパイル方式では言語の構文はそのままで、コンパイル前に付加情報を加えることでテストを集めています。

一方、最後の共有ライブラリから探す方法ではコンパイル後の共有ライブラリから情報を取得してテストを集めます。この方式では、テストを共有ライブラリとして作成し、テスト実行用のコマンドからその共有ライブラリを読み込み、テストを実行します。こうすることにより、テスト側にテスト登録処理を埋め込む必要がなくなります*2。共有ライブラリの中からテストを見つける処理はテスト実行コマンドが頑張るからです。

WinUnit

最初はWinUnitです。やり方は共有ライブラリからテストを集める方式なのですが、書き方はマクロを使う方式です。テストを定義するときはBEGIN_TESTEND_TESTで囲みます。

1
2
3
4
5
6
7
#include "WinUnit.h"

BEGIN_TEST(AddTest)
{
   WIN_ASSERT_TRUE(3 == add(1, 2));
}
END_TEST

すでにGoogle TestやBoost Test Libraryで見たように、この使い方であれば、共有ライブラリにする必要はありません。マクロの中で一工夫することでテストの自動登録を実現できるからです。

WinUnitの利点はVisual C++で使いやすいことでしょう。マクロを使ったAPIが気にならないVisual C++開発者には有力な選択肢です。

Cutter

最後はCutterです。CutterはC言語用の単体テストフレームワークとして開発されていましたが、先日リリースされた1.1.0で大きくC++対応を強化しています。

CutterではWinUnitとは違いマクロを利用しません。通常通り関数を定義するとテストとして認識されます。ただし、すべての関数がテストとして認識されるのではなく、test_からはじまる名前の関数だけをテストとして認識します。

1
2
3
4
5
6
7
8
9
#include <cppcutter.h>

namespace calc
{
   void test_add()
   {
       cppcut_assert_equal(5, add(2, 3));
   }
}

マクロを利用してテストを自動登録する方式では、フィクスチャ定義時に名前を揃える必要がありましたが、Cutterでは以下のようにnamespace内にsetup()/teardown()を定義するだけです。namespaceでグループ化されたテスト全体でフィクスチャを共有します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cppcutter.h>

namespace calc
{
   void setup() {...}
   void teardown() {...}

   void test_add() // calc::setup()/calc::teardown()が呼び出される
   {
       cppcut_assert_equal(5, add(2, 3));
   }

   void test_sub() // calc::setup()/calc::teardown()が呼び出される
   {
       cppcut_assert_equal(5, add(8, 3));
   }
}

この方式では通常のC++プログラムと同様にテストを書くことができるため、新しくテストを書くことの敷居が低くなります。しかし、tes_などタイプミスをしてしまった場合に、どうしてテストが実行されないのかに気づきにくいという問題点があります。

テストも通常のプログラムと同様に開発したい場合はマクロを使わないこの方式がオススメです。

まとめ

C++用の各種xUnitでのテストの書き方を、方式毎に分類して紹介しました。どんなバックグラウンドを持っているかにより、選びやすいxUnitは変わるでしょう。Visual C++開発者であればWinUnitを選ぶことが多いでしょうし、Qt開発者であればQtTestLibを選ぶことが多いでしょう。しかし、バックグラウンドから選ぶだけではなく、テストの書きやすさも判断材料に加えてみてはいかがでしょうか。

継続して開発すればそれに伴ってテストも増えていきます。しかし、テストは面倒くさがって飛ばしてしまいがちです。新しくテストを書く敷居が下がれば、テストを面倒くさがることが少なくなり、安心して開発を続けていくための土台を固めることができます。新しくテストを書く敷居を下げることは開発を継続するのであれば割に合うということです。

今回は「新しいテストの書きやすさ」を軸に様々なxUnitのやり方を紹介しました。C++用xUnitを選択する時の参考にしてみてください。

念のため書きますが、オススメはCutterです。

*1  プリコンパイルにはmoc(メタオブジェクトコンパイラ)を使います。

*2  「これがテストだよ」という目印は埋め込む必要があります。

タグ: Cutter | テスト
2009-11-07

UxUを用いたデータ駆動テストの記述

2009年10月30日付で、テスティングフレームワークUxUのバージョン0.7.5をリリースしました。

UxUはこれまで「Firefoxアドオン開発用テスティングフレームワーク」と銘打っていましたが、Thunderbird用アドオンの開発にも利用されていることと、バージョン0.7.0以降からXULRunnerベースのアプリケーション一般に対してインストール可能なようになったことから、現在のプロジェクトページ上では「Firefox/Thunderbird用アドオン・XULRunnerアプリケーション開発用テスティングフレームワーク」と表記しています。

バージョン0.7.0以降で、UxUはデータ駆動テストの記述に対応しました。今回はUxUでのデータ駆動テストの記述方法の解説を通じて、データ駆動テストの利便性についてご紹介したいと思います。

このエントリ内の目次:

  1. データ駆動テストとは?
    1. べた書きしたテスト
    2. 関数を使ったテスト
    3. ループを使ったテスト
    4. データ駆動テスト
  2. UxUでのデータ駆動テストの書き方
  3. データ駆動テスト用の便利な機能

データ駆動テストとは?

データ駆動テストとは、簡単に言えば、「テストのロジックとデータを分離した自動テスト」の事です。データ駆動テストの利点を理解していただくために、データ駆動テストではないテストの例もいくつか挙げながら、それぞれの利点と欠点を見ていきましょう。

べた書きしたテスト

以下は、XUL/Migemoという「ローマ字入力で普通の日本語の検索を行う」アドオンの中の、半角英数字によるローマ字入力をひらがなに変換するモジュールのテストの一部です。

1
2
3
4
5
6
7
8
9
10
11
12
13
function test_roman2kana() {
  assert.equals('あいうえお',      transform.roman2kana('aiueo'));
  assert.equals('aiueo',      transform.roman2kana('aiueo'));
  assert.equals('がぎぐげご',      transform.roman2kana('gagigugego'));
  assert.equals('にほんご',        transform.roman2kana('nihongo'));
  assert.equals('ぽーと',          transform.roman2kana('po-to'));
  assert.equals('きゃっきゃ',      transform.roman2kana('kyakkya'));
  assert.equals('うっうー',        transform.roman2kana('uwwu-'));
  assert.equals('\\(\\)\\[\\]\\|', transform.roman2kana('\\(\\)\\[\\]\\|'));
  assert.equals('\\(\\[',          transform.roman2kana('\\(\\['));
  assert.equals('\\)\\]',          transform.roman2kana('\\)\\]'));
  assert.equals('\\|',             transform.roman2kana('\\|'));
}

このモジュールのroman2kana()メソッドは、半角英数字のローマ字入力をひらがなに変換し、それ以外の入力はそのまま返すという仕様になっています。この仕様通りに動作するかどうかを検証するため、ここでは11種類の引数を渡し、戻り値をassert.equals()で検証しています。検証しないといけないパターンが増えた時には、行をコピーして引数と期待値の部分を書き換えることになります。

パッと見て分かるかと思いますが、このテスト用コードには以下のような問題があります。

  • 同じコードの繰り返しが多く冗長である
  • テスト対象のメソッドを呼び出す記述が直接書かれているため、メソッド名や引数の取り方が変わった時などには、11行すべてを書き換える必要がある

「テストのロジック」と「テストしなければいけないデータ」が一緒に記述されているため、メンテナンス性が低いコードになってしまっていると言えます。

関数を使ったテスト

このようなテストのメンテナンス性を高めるための方法の1つとしては、アサーションを行う部分を関数としてまとめておくというやり方が考えられます。例えば以下の要領です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function test_roman2kana() {
  function oneTest(aExpected, aInput) {
    assert.equals(aExpected,
                  transform.roman2kana(aInput));
  }
  oneTest('あいうえお',      'aiueo');
  oneTest('aiueo',      'aiueo');
  oneTest('がぎぐげご',      'gagigugego');
  oneTest('にほんご',        'nihongo');
  oneTest('ぽーと',          'po-to');
  oneTest('きゃっきゃ',      'kyakkya');
  oneTest('うっうー',        'uwwu-');
  oneTest('\\(\\)\\[\\]\\|', '\\(\\)\\[\\]\\|');
  oneTest('\\(\\[',          '\\(\\[');
  oneTest('\\)\\]',          '\\)\\]');
  oneTest('\\|',             '\\|');
}

コードの冗長さが減りました。また、メソッド名や引数の取り方が変わった時も、変更が必要な箇所は1箇所だけになりました。

これでも悪くはないのですが、実際にテストを繰り返し走らせていると、以下のような問題が浮き彫りになってきます。

  • テストする入力パターンのどれか1つでアサーションに失敗したら、そこから後の記述がスキップされてしまう

例えば以下のようなシナリオが考えられます。

  1. テストを実行した。
  2. 2番目のパターンでfailした。
  3. 2番目のパターンでfailする原因となっていたバグを直した。
  4. 再度テストを実行した。
  5. 5番目のパターンでfailした。
  6. 5番目のパターンでfailする原因となっていたバグを直した。
  7. 再度テストを実行した。
  8. 6番目のパターンでfailした。
  9. …(以下続く)

「3の段階で行った修正で5や8のバグが発生した」という可能性もありますが、最初から2や5や8のバグがあったのであれば、まとめて直せていたかもしれません。これでは、バグを直しても直してもきりがないという、モグラ叩きのような感覚に陥ってしまいます。

ループを使ったテスト

メンテナンス性を高める別の手法として、アサーションに使う期待値とメソッドに渡す引数だけを配列で別途定義しておくというやり方も考えられます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test_roman2kana() {
  var patterns = [
        ['あいうえお',      'aiueo'],
        ['aiueo',      'aiueo'],
        ['がぎぐげご',      'gagigugego'],
        ['にほんご',        'nihongo'],
        ['ぽーと',          'po-to'],
        ['きゃっきゃ',      'kyakkya'],
        ['うっうー',        'uwwu-'],
        ['\\(\\)\\[\\]\\|', '\\(\\)\\[\\]\\|'],
        ['\\(\\[',          '\\(\\['],
        ['\\)\\]',          '\\)\\]'],
        ['\\|',             '\\|']
      ];
  for each (var pattern in patterns) {
    assert.equals(pattern[0],
                  transform.roman2kana(pattern[1]));
  }
}

これにもやはり問題があります。

  1. テストする入力パターンのどれか1つでアサーションに失敗したら、そこから後に記述された入力パターンに対するアサーションが行われない。(関数を使った書き方の場合と同じ問題)
  2. 配列の内容の順番を覚えておかないといけない
  3. failした時に、どの入力パターンでfailしたのかが分からない
  4. 1、2、3の問題を回避するための対処を毎回しなければならないのが面倒

1つ目の問題点については前述したとおりです。

2つ目の問題は、この例のように2次元配列を使った場合に現れます。上記の例では配列の0番目の要素が期待値、1番目の要素が入力となっていますが、これではどっちがどっちなのかを常に意識する必要があります。

3つ目は、ループに特有の問題です。UxUではテストにfailした時にスタックトレースが表示され、べた書きした場合や関数を使った書き方の場合であれば、スタックトレースを辿れば「どのパターンで失敗したのか」の情報に辿り着くことができます。しかし、ループを使っていると、スタックトレースの行き着く先はループの中になってしまうため、どのパターンに対して失敗したのかが一目では分からなくなってしまいます

この対策として、UxUのアサーションでは最後の引数として任意のメッセージを渡せるので、以下のようにして「どのパターンで失敗したのか」を表示させることは可能です。

1
2
3
assert.equals(pattern.expected,
              transform.roman2kana(pattern.input),
              pattern.input+'に対するテスト');

しかし、実際にたくさんテストを書くようになってくると、これが地味に面倒です。これが4つ目の問題点です。

べた書きした場合や関数を使った書き方の場合であれば、このような配慮なしに淡々とテストを書いていても、テスト実行時にはスタックトレースを辿ればデバッグに必要な情報を得られます。それなのに、ループに対してはこのような配慮をしなければいけないわけです。この面倒さによって、テストを新しく書いたり過去に書いたテストをメンテナンスしたりする意欲がじわじわと削がれてしまう、というのが一番の問題点だと言えます。

データ駆動テスト

UxU 0.7.0以降で導入されたデータ駆動テストの仕組みを使うと、上記のテストはこのように書くことができます。

1
2
3
4
5
test_roman2kana.parameters = utils.readParametersFromCSV('patterns.csv');
function test_roman2kana(aParameter) {
  assert.equals(aParameter.expected,
                transform.roman2kana(aParameter.input));
}

テスト用のコードにはロジックだけを書き、データは外部ファイルで定義します(後述しますが、テストケース内にデータを埋め込むこともできます)。データを定義しているファイルの形式はCSVです。

patterns.csvの内容
 inputexpected
半角英数aiueoあいうえお
全角英数aiueoaiueo
濁音のみgagigugegoがぎぐげご
濁音混じりnihongoにほんご
音引きpo-toぽーと
拗音kyakkyaきゃっきゃ
撥音uwwu-うっうー
paren\\(\\)\\[\\]\\|\\(\\)\\[\\]\\|
parenOpen\\(\\[\\(\\[
parenClose\\)\\]\\)\\]
pipe\\|\\|

この時、UxUは「test_roman2kana」という1つのテストではなく、「test_roman2kana (半角英数)」「test_roman2kana (全角英数)」……という名前の11個のテストを実行するようになります。

  • コードの繰り返しが無く、簡潔に書ける
  • すべてが独立したテストとして扱われるので、入力パターンのどれか1つでアサーションに失敗しても、他のテストはスキップされない
  • (CSVの場合)カラム名が引数として渡ってくるハッシュのキーとなるので、引数の順番を覚えておかなくていい
  • failした時は、実行されたテストの名前の中に実行時の入力パターンの名前が含まれるので、どのパターンで失敗したのかがすぐ分かる
  • これらをフレームワークの機能として提供しているので、テストを記述する時に面倒なことを考えなくてもよい

このように、データ駆動テストには多くのメリットがあります。単純な入出力のパターンを数多く検証しなければいけない場面で、データ駆動テストは威力を発揮します。

UxUでのデータ駆動テストの書き方

UxUでは、テスト関数のparametersプロパティに配列またはハッシュを代入すると、そのテストをデータ駆動テストとして実行するようになります。この時テスト関数には引数として、parametersプロパティの配列またはハッシュの要素が1つずつ渡されます。

以下は、配列を指定した場合の例です。

1
2
3
4
5
6
7
8
9
10
11
12
13
test_someFunc.parameters = [
  { expected : '29',   input : 'niku' },
  { expected : '2929', input : 'nikuniku' },
  { expected : '029',  input : 'oniku' }
];
function test_someFunc(aParameter) {
  /*
    1回目: aParameter = { expected : '29',   input : 'niku' }
    2回目: aParameter = { expected : '2929', input : 'nikuniku' }
    3回目: aParameter = { expected : '029',  input : 'oniku' }
  */
  ...
}

ハッシュを指定した場合は、以下のようになります。ハッシュのキーはテスト関数には渡されず、結果を表示する時のテスト名として表示されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
test_someFunc.parameters = {
  simgle: { expected : '29',   input : 'niku' },
  double: { expected : '2929', input : 'nikuniku' },
  o:      { expected : '029',  input : 'oniku' }
};
function test_someFunc(aParameter) {
  /*
    1回目: aParameter = { expected : '29',   input : 'niku' }
    2回目: aParameter = { expected : '2929', input : 'nikuniku' }
    3回目: aParameter = { expected : '029',  input : 'oniku' }
  */
  ...
}

データ駆動テスト用の便利な機能

データ駆動テストのサポートに併せて、いくつかの新しいヘルパーメソッドが追加されています。これらを使うことで、データ駆動テストをより簡単に作成・メンテナンスすることができます。

前述の例で使用しているutils.readParametersFromCSV()は、CSVファイルの内容を読み込み、最初の行のカラム名をキーとしたハッシュとして返します。例えば前述の例のCSVは、以下のようなハッシュとして解釈されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// test_roman2kana.parameters = utils.readParametersFromCSV('patterns.csv');
// これは、以下のように書くのと同じ
test_roman2kana.parameters = {
  '半角英数':   { input: 'aiueo',           expected: 'あいうえお' },
  '全角英数':   { input: 'aiueo',      expected: 'aiueo' },
  '濁音のみ':   { input: 'gagigugego',      expected: 'がぎぐげご' },
  '濁音混じり': { input: 'nihongo',         expected: 'にほんご' },
  '音引き':     { input: 'po-to',           expected: 'ぽーと' },
  '拗音':       { input: 'kyakkya',         expected: 'きゃっきゃ' },
  '撥音':       { input: 'uwwu-',           expected: 'うっうー' },
  paren:        { input: '\\(\\)\\[\\]\\|', expected: '\\(\\)\\[\\]\\|' },
  parenOpen:    { input: '\\(\\[',          expected: '\\(\\[' },
  parenClose:   { input: '\\)\\]',          expected: '\\)\\]' },
  pipe:         { input: '\\|',             expected: '\\|' }
};

CSVファイルはRFC4180準拠の形式の読み込みに対応しています。カンマ区切りではなくタブ区切りのファイルを使用したい場合は、utils.readParametersFromTSV()を使用して下さい。どちらも、読み込みたいファイルのパス(相対パスも利用できます)を第1引数に、ファイルのエンコーディングを第2引数に指定します。エンコーディング指定を省略した場合はUTF-8として読み込みます。

以下のリンク先に、各メソッドの詳しい説明があります。

また、JSON形式で保存した外部ファイルを読み込むためのutils.readJSON()というメソッドもあります。

1
test_roman2kana.parameters = utils.readJSON('patterns.json');

こちらも、読み込みたいファイルのパス(相対パスも利用できます)を第1引数に、ファイルのエンコーディングを第2引数に指定します。エンコーディング指定を省略した場合はUTF-8として読み込みます。詳しい説明は以下のリンク先をご覧下さい。

まとめ

データ駆動テストの仕組みを利用すると、テストのロジックとデータを分離できるため、メンテナンス性が高まることが期待できます。様々なパターンの入力を受け付ける機能を開発する時は、データ駆動テストをぜひ一度試してみて下さい。

タグ: Mozilla | UxU | テスト
2009-10-30

(tokyo-emacs #x02)の資料公開

クリアコードのインターンシップに参加したはやみずさんが主催する(tokyo-emacs #x02)で使った資料を公開しました。

現在のところ、まだ、上記の一覧ページには発表者全員の資料が揃っていませんが、おいおい揃っていくでしょう。参加できなかった方は公開された資料で内容を感じてみてください。ただ、当日はデモまじりの発表も多かったため、資料だけでは伝わらない部分もあるでしょう。

(tokyo-emacs #x02) レポまとめから辿れるレポートを読めば、資料からだけではわからないことも垣間見れるでしょう。資料だけ、レポートだけで物足りない場合は、次回は参加してみてはいかがでしょうか。

デモ

発表ではテスト起動を支援するrun-test.elのデモをするという名目でRabbitのデモをしました。

会場にRabbitユーザはほとんどいませんでしたが、Rabbitユーザにとってはよだれものだったはずです。

自動サイズ調整

簡単なデモであればスライドに穴を開けてデモをしますが、今回のようにEmacsでコードを書きながらのデモではスペースが小さくなってしまいます。また、穴を開けるとスライドを参照することができないという問題もあります。

今回のデモではウィンドウのサイズを変えて、縦は最大化・横は200px程度とし、画面左にスライド、右にEmacsという並びにしました。Rabbitは縦横比が変わってもそれっぽくレンダリングするので、今回のような使い方ができます。

Rabbitでデモをするときは、穴を開けるだけではなく、ウィンドウサイズを変えることも検討してみてください。

rabbit-commmand

RabbitにはdRubyなどでRabbitを遠隔操作するrabbit-commandというコマンドが付属しています。また、Emacs上でRabbit用スライドを編集するためのrabbit-modeにはrabbit-commandを利用してスライドを進めるコマンドが用意されています。

今回はスライドに書かれている内容を実際に動かしながらデモを行ったため、スライドの内容を取得できるとタイピングしなくてもよいためとても便利です。そこで、trunkにrabbit-command経由で現在のスライドの内容を取得するコマンドを追加しました。デモでは、EmacsからC-uM-!でrabbit-commandを呼び出すことにより、スライドの内容をEmacsに貼り付けてタイピングの手間を省きました。

もちろん、次のスライドへの移動はrabbit-modeのコマンドを利用しました。

現在のスライドの内容を取得するコマンドは以下の通りです。Rabbitを使った発表でコードを書くようなデモをするときに役立つでしょう。

% rabbit-command --current-slide-rd

まとめ

はやみずさんが、tokyo-emacsはみんなで作るものだ、ということを強調していたのが印象的でした。11月にあるらしい(tokyo-emacs #x03)に参加して、tokyo-emacsを作ってみてください。

Emacsに少しでも関連していれば発表側で参加することもできそうな雰囲気でした。発表側でtokyo-emacsを作るのも面白いかもしれませんよ。

タグ: テスト
2009-09-09

Cutter 1.0.8リリース

本日、C言語用単体テストフレームワークであるCutterの新バージョン1.0.8が肉リリースされました。

新機能

先日の2つの記事(その1その2)でも紹介しましたが、1.0.8の重要な新機能はHTTPインターフェースのテスト機能 SoupCutter です。SoupCutter では GNOMEプロジェクトで開発されている HTTPサーバー・クライアントライブラリの libsoup をバックエンドに利用しており、HTTPサーバーやクライアントプログラムのテストを簡単に記述できるようになっています。

SoupCutter の使い方は、前回の紹介記事に詳しく書いてあるので是非これを参考に使ってみてください。この記事では SoupCutterの使用例として groonga のHTTPインターフェースのテストを作成しているのですが、SoupCutter を使った HTTP インターフェースのテストは実際に groonga 本体にも取り込まれています。

また、1.0.8からFedoraのrpmパッケージや、Mac OS Xのportsパッケージ(MacPorts)、Debian、Ubuntuのdebパッケージもサポートするようになったので、これまでよりも手軽に Cutter を導入できるようになりました。

まとめ

汎用的なHTTPのライブラリを使ったとしても、C言語でHTTPインターフェースのテストを開発しようと思うと一手間かかってしまうのではないかと思います。しかし、HTTPのテストに特化したSoupCutterを利用すれば簡潔にテストを記述できる上に、その気になれば libsoup の豊富な機能をフル活用することもできるようになります。

ますます便利になった Cutter を使って、皆さんが関わっているプロジェクトのテストを作成してみませんか?

つづき: 2009-12-21
タグ: Cutter | テスト
2009-08-29

SoupCutter で全文検索エンジンgroongaのHTTPインターフェースのテスト作成

前回に引き続き、クリアコードインターン記事の2回目です。前回の記事で紹介したCutterのHTTPテストモジュールであるSoupCutterを使って、全文検索エンジンgroongaのHTTPインターフェースのテストを作成したので、今回はその紹介をしたいと思います。SoupCutterが実際どのように使えるかという実例として、よい題材なのではないかと思います。

全文検索エンジンgroongaはHTTPサーバー機能を備えており、Webブラウザからアクセスすることでテーブルを作成したり、データベースの中身を調べたりすることができます。このようにブラウザからデータベースを管理するために、groongaではデータベースを操作するための基本的なAPIをHTTPリクエストによって呼び出せるようにしています。例えば、localhost:10041 で groonga のサーバーを実行しているときに、http://localhost:10041/table_list を GET すると、テーブルの一覧を取得することができます。

Cutterでテスト開発:下準備

それでは、Cutter でどのようにしてテストを開発していくかを見ていきましょう。

今回は groonga のHTTPサーバー機能のテストを行うため、まずは groonga でHTTPサーバーを走らせなければなりません。Cutterには外部コマンドを簡単に扱うことができる GCutEgg というオブジェクトがあります。groonga でポート 4545 を listen する HTTPサーバーを起動するには、以下のようなコマンドを実行します。

groonga -s -p 4545 -n /path/to/dbfile

このコマンドをテストのプログラムから実行しなければなりません。GCutEgg を使うと、以下のように簡単にコマンドを実行することができます。

1
2
3
GCutEgg *egg = gcut_egg_new("groonga", "-s", "-p", "4545",
                            "-n", "/path/to/dbfile", NULL);
gcut_egg_hatch(egg, NULL);

たったこれだけで、簡単に groonga のHTTPサーバーを準備することができました。このサーバーはテストの間は実行していて、テストが終わるごとに終了してほしいので、setup で実行を始めて、tear down で終了してあげればよいでしょう。

また、前回の記事で紹介した Cutter のHTTPクライアント SoupCutClient も setup で準備しておくとよいでしょう。

1
2
client = soupcut_client_new();
soupcut_client_set_base(client, "http://localhost:4545/");

soupcut_client_set_base で SoupCutClient にベースURIを設定しておくことで、実際にGETリクエストを送信するときのURI指定で楽をすることができます。SoupCutter では、soupcut_client_get(client, "http://localhost:4545/path/to/something") のようにGETリクエストを送ることができるのですが、ベースURIを設定ておけば soupcut_client_get(client, "/path/to/something") と書くだけで、 http://localhost:4545/path/to/something にGETリクエストを送ることができるようになります。

さらにもうひとつ。GCutEgg と SoupCutClient はどちらも GLib のオブジェクトとして実装されており、解放するときは g_object_unref を呼ぶだけでデストラクタが呼ばれ、適切にオブジェクトを解放してくれます。Cutter では、オブジェクトを破棄する関数と共にオブジェクトを登録しておくと、テストの tear down 時に自動でオブジェクトを解放してくれるという便利機能があります。どうやるかというと、下記のようにするだけです。

1
2
cut_take(client, g_object_unref);
cut_take(egg, g_object_unref);

またこれらは、GLibをサポートしたGCutterの関数を使うと、

1
2
gcut_take_object(G_OBJECT(client));
gcut_take_object(G_OBJECT(egg));

と書くこともできます。 これで client と egg は自動的に tear down 時に解放されるようになります。Cutterでは適切な下準備をしておくと、tear down 用の関数でわざわざ後片付けをしなくてもOKです。便利ですね。

ここまでをまとめると、テストのセットアップは次のように書くことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static GCutEgg *egg;
static SoupCutClient *client;

void
cut_setup(void)
{
  client = soupcut_client_new();
  soupcut_client_set_base(client, "http://localhost:4545/");
  gcut_take_object(G_OBJECT(client));

  egg = gcut_egg_new("groonga", "-s", "-p", "4545", "-n", "/tmp/http.db", NULL);
  gcut_egg_hatch(egg, NULL);
  gcut_take_object(G_OBJECT(egg));

  g_usleep(G_USEC_PER_SEC); /* groonga の listen が完了するまで適当な時間待つ */
}

SoupCutterでテスト開発:HTTPサーバーが実行できているかのテスト

まずは簡単なところからテストしていきましょう。groongaのHTTPサーバーはルートにGETリクエストを送ると、本文無しで 200 STATUS OK を返してくるのでこれをテストしてみます。

1
2
3
4
5
6
7
8
9
void
test_get_root(void)
{
  soupcut_client_get(client, "/", NULL); /* http://localhost:4545/ を GET */

  soupcut_client_assert_response(client); /* status code は 2XX かチェック */
  soupcut_client_assert_equal_content_type("text/javascript", client); /* Content-Type をチェック */
  soupcut_client_assert_equal_body("", client); /* 本文が空かをチェック */
}

このように、非常に簡潔にテストを書くことができます*1

もう1つテストを書いてみましょう。groongaのHTTPサーバーは、/status にリクエストを送ると、{"starttime":1251190614,"uptime":39} というようにサーバーが開始した時刻とuptime をJSON形式でレスポンスとして応答します。starttimeもuptimeも開始した時刻や現在時刻によって刻々と変化するため、単純に assert_equal_body で期待した文字列と一致するかどうかを調べるには無理があります。このような要求に答えるために、SoupCutterでは正規表現に本文がマッチするかをテストできる soupcut_client_assert_match_body という関数を提供しています。

1
2
3
4
5
6
7
8
9
10
void
test_get_status(void)
{
  soupcut_client_get(client, "/status", NULL);

  soupcut_client_assert_response(client);
  soupcut_client_assert_equal_content_type("text/javascript", client);
  soupcut_client_assert_match_body("{\"starttime\":\\d+,\"uptime\":\\d+}",
                                   client);
}

soupcut_client_assert_match_body を利用すると、このようにして /status をGETしたときのテストを実装することができます。

このように、柔軟なテストも簡単に作成できるのが Cutter の特徴であり、開発方針でもあります。

その他のHTTPインターフェースのテストも、groongaの側でテーブルを作っておいたりカラムを作っておいたりというコードを書かなければならないことを除けば、ほとんど上記の2つのテストと同様に開発してゆくことができます。テーブルを作成する API は、/table_create にクエリーパラメータとしてテーブル作成に必要な情報を渡すことで呼び出すことができますが、これも SoupCutter では次のように簡潔に書くことができます。

1
2
3
4
5
6
7
8
soupcut_client_get(client,
                   "/table_create",
                   "name", "newtable1",
                   "flags", flags,
                   "key_type", "Int8",
                   "value_type", "Object",
                   "default_tokenizer", "",
                   NULL);

まとめ

今回はテストを開発する実例を通して、SoupCutter の使い方について紹介しました。SoupCutter を使って開発された groonga のテストは、実際に groonga のレポジトリにも取り込まれています。

SoupCutter を含めた Cutter は今週中にリリース予定なので、是非みなさん使ってみてください。

*1  関数名がやや長いのは御愛嬌ということで ;-)

タグ: テスト | Cutter
2009-08-26

C言語用単体テストフレームワークCutterへのHTTPテスト機能追加

現在クリアコードでインターン中のはやみずです。クリアコードのインターンシップ制度は今年度から始まり、最初のインターン生として2週間クリアコードで働かせていただくことになりました。今回と次回の2つの記事で、現在インターンシップで取り組んでいる内容について紹介したいと思います。今回の記事では、C言語用単体テストフレームワークCutterへのHTTPテスト機能追加について紹介します。

Cutter: C言語用単体テストフレームワーク

ククログでも度々紹介されているCutterですが、このテストフレームワークを利用することでC言語での単体テストを非常に効率良く開発することができます。

Cutterはできるだけ簡潔にテストを記述できるように、基本的な assert 系関数以外にも様々なユーティリティ関数を提供しています。また、GLibを利用したプログラムのテストを開発するためのGCutterや、gdk-pixbuf(C言語で画像を扱うためのライブラリ)用の GdkCutter Pixbufなどのモジュールが含まれています。これらを利用することで、GLib や Gdk Pixbuf を使ったプログラムはよりこれらのライブラリに特化したテストを簡単に書くことが可能となっています。これからも対応ライブラリは増えてゆくのかも?

Cutterの強みの1つに、C言語でありながらメモリ管理の手間が非常に少ないということが挙げられます。ほとんどのテストフレームワークは set up(準備)→test実行→tear down(後片付け) という処理の流れを基本としているので、テスト中に利用するオブジェクトは tear down のときに解放してやればよいことがわかっています。Cutter は「このオブジェクトは tear down 時に解放しといてね」ということを Cutter に教えるための API を提供しているため、この API を利用することで解放忘れによるメモリリークを防ぐことができます。例えば文字列であれば、cut_take_string(const gchar *string) を利用することで文字列を動的にアロケートした領域にコピーしてそのポインタを得ることができ、tear down時にはこの文字列が自動的に解放されます。

今回開発しているCutterのHTTPテスト機能も、このパターンを使ってオブジェクトを簡単に生成して、しかも勝手に解放してくれるようになっています。

SoupCutter: CutterのHTTPテストモジュール

さて、本題のHTTPテスト機能を実装した SoupCutter に話を移しましょう。今回の開発では、HTTPの機能を簡単に実装するために libsoup というライブラリを利用しました。というよりも、GLibをサポートするのが GCutter、Gdk Pixbuf をサポートする GdkCutter Pixbuf などのように、libsoup をサポートする SoupCutter という位置付けのほうが正確です。しかし、簡単なテストであれば libsoup 自体には一切触れることなく作成することができるので、Cutter で HTTP サーバーや HTTP クライアントのテストを簡単にできるようにするためのモジュールだと思っていただいても大丈夫です。

SoupCutter を使って HTTP サーバープログラムが正しくレスポンスを返しているかをテストするには、次のように書くことができます。

1
2
3
4
5
6
7
8
9
SoupCutClient *client = soupcut_client_new();

/* http://localhost:8080/?key=value に HTTP Request を送信 */
soupcut_client_get(client, "http://localhost:8080/",
                   "key", "value", NULL);

soupcut_client_assert_response(client);
soupcut_client_assert_equal_content_type("text/plain", client);
soupcut_client_assert_equal_body("Hello, world", client);

SoupCutClient というのは、サーバーとやりとりしたHTTPリクエスト/レスポンスを内包しているオブジェクトです。..._assert_response では、最後に受け取ったレスポンスが 2XX (200 OK など) であるかをチェックしています。同様に、..._assert_content_type では Content-Type が text/plain であることを、..._assert_equal_body ではレスポンスの本文が Hello, world であることをチェックしています。このようにして、SoupCutter を使うと非常に簡潔な記述で HTTP サーバーが思った通りに動いているかを調べることができるようになっています。

また、SoupCutter の最初の機能としては HTTP サーバーのテスト、つまりHTTPクライアントとしての機能を実装しているのですが、このHTTPクライアント機能が正しく動作しているかをテストしなければなりません。HTTPクライアント機能をテストするためには、HTTPサーバが必要です。HTTPサーバーをテストするためにHTTPクライアントを実装し、そのHTTPクライアントをテストするためにHTTPサーバーを実装する。ややこしいですね。

というわけで、SoupCutter は簡単にHTTPサーバーを作ることもできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
server_callback (SoupServer *server,
                 SoupMessage *msg,
                 const gchar *path,
                 GHashTable *query,
                 SoupClientContext *client,
                 gpointer user_data)
{
    .... /* リクエストを処理して結果を返す */
}

SoupServer *server;
server = soupcut_server_take_new(NULL);
soup_server_add_handler(server, "/", server_callback,
                        NULL, NULL);
soup_server_run(server);

HTTPサーバーの作成自体はたった3行でできてしまいました。server_callback は実際にリクエストを処理してレスポンスを生成するコールバック関数です。これを SoupServer のリクエストハンドラに追加して、soup_server_run でメインループに入り、サーバーが動き始めます。

ここで注目してほしいのは、サーバーを生成するときの soupcut_server_take_new です。Cutterでは take と名前のつく関数で生成したオブジェクトは、tear down時に自動で解放されます。HTTPサーバーの場合は、ちゃんとソケットの後処理まで行い、オブジェクトを解放してくれます。つまり、HTTPサーバーを簡単に作れるだけではなく、勝手に後片付けまでしてくれます。

まとめ

今回は、現在開発中である Cutter の HTTPテストモジュール SoupCutter について簡単に紹介しました。SoupCutterを使うと簡単便利にHTTPサーバー/クライアントのテストを作成することができるようになります。SoupCutter は現在クリアコードのインターンシップで開発していて、来週末に SoupCutter を含めた Cutter をリリースすることを目標に頑張っています。もし HTTP のテストを作る必要に迫られた場合には、SoupCutter を検討してみてください。

タグ: テスト | Cutter
2009-08-21

Pikzie 0.9.5リリース

Python用の書きやすくデバッグしやすい単体テスティングフレームワークPikzie 0.9.5をリリースしました。

easy_installでもpipでもインストールできます。

% sudo easy_install Pikzie

または

% sudo pip install Pikzie

今回は、Windowsでもある程度動くようになったのでリリースしました。

新機能はテストを省略するomitの追加です。

タグ: テスト
2009-07-23

Pikzie 0.9.4リリース

肉の日なのでPikzie 0.9.4をリリースしました。

0.9.4

今回のリリースではとくに新機能はなく、 hexacosa.net::Pikzie (unittesting extention module)で教えてもらったバグが修正されている程度です。

Pythonでコードを書く機会があるとPikzieの機能も増えると思いますが、最近はなかなかPythonを使う機会がないため新機能が増えていません。新機能追加案やそのような機能を実装したパッチはWelcomeなので、使ってみて足りていない便利そうな機能があれば教えてください。

PikzieはPython Package Indexにも登録しているため、easy_installやpipを使って簡単にインストールすることができます。

easy_installを使う場合:

% sudo easy_install pikzie

pipを使う場合:

% sudo pip install pikzie

簡単に試すことができますね。

Pikzieの提供したい機能

新機能はWelcomeなのですが、「テストを書きつづけること」に邪魔になりそうな機能は受け入れないかもしれません。邪魔になりそうな機能とは「デバッグしづらくなる機能」や「テストが読みづらくなる機能」などです。

Pythonで広く使われているであろうnoseはプラグイン機能があり、たくさんの機能を備えています。例えば、assert*だけではなく、できるだけタイピング数を減らすためにokeqといった機能も提供されています。

1
2
ok(a == b) # == assert(a == b)
eq(a, b) # == assert(a == b, "%r != %r" % (a, b))

しかし、これらは「テストを書きつづけること」の邪魔になりそうな機能だと思います。そのため、もし、Pikzieにeqokを追加してほしいという要望があった場合は断るでしょう。

okは「デバッグしづらくなる機能」だと思います。上記の例では、okが失敗したとき、abの値がなんだったのかを示してもらえません。問題を解決するためには、何が問題かを把握する必要があり、そのためには問題解決につながるエラーメッセージが非常に重要です。その情報を提供しないokを簡単に使えるようにすると、デバッグしづらいテストを書いてしまいます。

eqは「テストが読みづらくなる機能」だと思います。簡潔に書いてあるプログラムは読みやすいですが、省略した名前を使って短く書かれたプログラムは読みづらいものです。何を意図しているかが明確ではないからです。1つ省略した名前を使うと他でも省略した名前を使いたくなります。そのため、eqを簡単に使えるようにすると読みづらいテストを書いてしまいます。

注意: 名前は長ければよいというものではありません。最小限の量で必要な情報が過不足なくこめられている名前がよい名前です。そのためには、その名前が使われている文脈を意識することが重要です。いつか、名前の話も書きたいものです。

Pikzieの目指すところ

noseもPikzieもテストを書きやすくすることに重点がおかれています。 そのため、noseもPikzieもたくさんの機能を提供しています。

noseとPikzieの違いは「テストを書きつづけること」にも重点がおかれているかどうかです。Pikzieは「テストを書きつづけること」にも重点がおかれているため、それを阻害するような機能を提供していません。

機能が多いことを重視する場合はnoseを選択するのがよいでしょう。しかし、テストを書きつづけることを重視する場合はPikzieもよい選択肢になると思います。

肉の日リリース情報

Pikzie以外にも高橋メソッドなプレゼンツール in XUL リターンズがリリースされています。

高橋メソッドなプレゼンツール in XUL リターンズの新機能は明日のMozilla Japan JP 10.0でわかるでしょう。

タグ: テスト
2009-05-29

UxUで外部テキストエディタを使う時のおすすめ設定

みなさん、テストしてますか?(挨拶)

UxUでは、テスト失敗時に表示されるスタックトレースからテキストエディタを起動することができます。この時、利用するテキストエディタがコマンドライン引数による行指定に対応していれば、エラーが発生した行を直接開いて編集できます。きちんと設定しておけば、テストを実行して、編集して、またテストして、といったサイクルで開発を進められるので非常に便利です。

以下に、有名なテキストエディタ向けの設定の例をいくつか挙げてみました。UxUの設定ダイアログの「MozUnitテストランナー」タブでエディタ起動用のコマンドとして入力してください。(エディタの実行ファイルのパスは必要に応じて読み替えてください)

秀丸エディタ
"C:\Program Files\Hidemaru\Hidemaru.exe" /j%L,%C "%F"
TeraPad
"C:\Program Files\TeraPad\TeraPad.exe" /j=%L "%F"
サクラエディタ
"C:\Program Files\sakura\sakura.exe" "%F" -X=%C -Y=%L
EmEditor
"C:\Program Files\EmEditor\EmEditor.exe" /l %L /cl %C "%F"
xyzzy
"C:\Program Files\xyzzy\xyzzycli.exe" -l "%F" -g %L -c %C
萌エディタ
"C:\Program Files\moeditor\moe.exe" "%F" -m %L,%C
gedit
/usr/bin/gedit +%L "%F"
Vim(vi)
/usr/bin/vim +%L "%F"
Emacs
/usr/bin/gnuclient +%L "%F"

なお、%Lは行番号、%Cは列番号、%Fはファイルのパスへと、それぞれ自動的に置換されます。

タグ: Mozilla | テスト | UxU
2009-04-01

Cutter 1.0.6リリース

先日、C言語用単体テストフレームワークであるCutterの新バージョン1.0.6がリリースされました。(アナウンス

ハイライト

NEWSにも書いてあるように、今回のリリースでも多くの新機能がありますが、ここではその中でも特におすすめの構造体定義なしで複雑なテストデータを使えるAPIを紹介します。

この新機能によりもっと簡単にデータ駆動テストが書けるようになります。実際、milter managerのテストが簡単に書けるようになりました。

データ駆動テスト

データ駆動テストとは同じテストケースに対して複数のテストパターンを適用するテスト手法です。これにより多くのテストパターンを簡単に書くことができるという利点があります。

例えば、入力された文字列の小文字をすべて大文字に変換するto_upper()関数のテストをするとします。テストデータは以下の3つを考えます*1

  • すべて小文字の文字列
  • 小文字と大文字が混ざった文字列
  • すべて大文字の文字列

これはこのように書けます。

1
2
3
cut_assert_equal_string("HELLO", to_upper("hello"));
cut_assert_equal_string("HELLO", to_upper("HelLo"));
cut_assert_equal_string("HELLO", to_upper("HELLO"));

データ駆動テストでは、テストケースはこのようになります。

1
cut_assert_equal_string(expected, to_upper(input));

このうち、expectedとinputを外部から与えることになります。Cutterではこのように書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void
data_to_upper (void)
{
#define ADD_DATUM(label, expected, input)             \
  gcut_add_datum(label,                               \
                 "expected", G_TYPE_STRING, expected, \
                 "input", G_TYPE_STRING, input,       \
                 NULL)

    ADD_DATUM("all lower", "HELLO", "hello");
    ADD_DATUM("mixed", "HELLO", "HelLo");
    ADD_DATUM("all upper", "HELLO", "HELLO");

#undef ADD_DATUM
}

void
test_to_upper (gconstpointer data)
{
    const gchar *expected, *input;

    expected = gcut_data_get_string(data, "expected");
    input = gcut_data_get_string(data, "input");
    cut_assert_equal_string(expected, to_upper(input));
}

QtのQtTestLibではこのようになります。 Chapter 2: Data Driven Testingより:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void TestQString::toUpper_data(QtTestTable &t)
{
    t.defineElement("QString", "string");
    t.defineElement("QString", "result");

    *t.newData("all lower") << "hello" << "HELLO";
    *t.newData("mixed")     << "Hello" << "HELLO";
    *t.newData("all upper") << "HELLO" << "HELLO";
}

void TestQString::toUpper()
{
    FETCH(QString, string);
    FETCH(QString, result);

    COMPARE(string.toUpper(), result);
}

ほとんど同じくらいの手間で書けているのではないでしょうか。

テストプログラムは重複があってもいいからわかりやすい方がよい、とよく言われます。しかし、そのために、重複がたくさんある中から、興味がある重複していない部分が見つけづらくなるのは問題かもしれません。データ駆動テストでは、どのようにテストしたいのかがわかりやすいテストケースになる傾向がある気がしています。

データ駆動テストのサポート状況

多くのテスティングフレームワークはデータ駆動テストをサポートしています。PerlのTest::Base、RubyのRSpec*2、C++のQtのQtTestLibやGoogle Testでもサポートされています。他にもまだまだたくさんあります。

テストデータの入力方法も様々で、テストプログラム中でテストデータを生成するものから、データベースからテストデータを取り出すもの、CSV、Excelなどから取り出すものもあります。

まとめ

動的に複雑なことができるスクリプト言語、アノテーションなどでメタデータを指定できる最近の言語、トリッキーなC++などでは便利にデータ駆動テストを実行できるテスティングフレームワークは多くあります。しかし、C言語用のフレームワークではそんなになかったのではないかと思います。*3

Cutterに興味がある方は使ってみたり、メーリングリストなどで提案、質問などしてみてください。

*1  他にも空文字列やアルファベット以外が入った文字列などが考えられます。

*2  動的にspecを定義してね、という方針のようなのでサポートしているというか、Rubyを使っているからできちゃうという感じ

*3  少なくともまだ見つけられていない。C言語用のテスティングフレームワークのAPIや機能に関してまとまっている場所はあるのだろうか。

タグ: Cutter | テスト
2009-03-02

UxU(UnitTest.XUL)を利用したFirefoxアドオンのデバッグの例

Firefoxアドオン開発者向け自動テストツールのUxUは、新たに発見したバグの修正にも活用することができます。本日リリースされたXUL/Migemo バージョン0.11.7で行われた修正の場合を例に、実際のデバッグ作業の流れを解説します。

状況

XUL/Migemoは、Firefoxで表示しているページ内のテキストを検索する機能を提供するアドオンですが、検索を開始する際に、「現在のスクロール位置から検索を開始する」という処理を含んでいます。0.11.6以前では、この機能を使用している時に、ページ先頭から検索が始まるべき場面で、先頭以外の場所から検索が始まってしまうことがあるという問題が起こっていました。

再現条件の特定

いくつか条件を変えて調査した結果、スクロールが発生しているページでは期待通りの結果になっているのに対して、スクロールが全く発生していないページ(ページ全体がウィンドウの現在の大きさの中に収まっているページ)では期待と異なる結果になっていることが判明しました。

原因箇所の特定

前述の処理の肝となっているのは、pXMigemoFindクラスfindFirstVisibleNode()というメソッドです。このメソッドは、渡されたフレーム(DOMWindow)において現在のスクロール位置で見えている最初の要素を検索して返す物です。このメソッドの戻り値を確認した所、前述の条件下では戻り値が期待と異なっている事が判明しました。

このことから、今回主な修正対象になるのはこのfindFirstVisibleNode()というメソッドであるということになります。

テストケースの作成

上記メソッドの実装を見直す前に、UxU用のテストケースを作成します。これにより、これから行う修正で目指すべきゴールが明確になります。つまり、このテストが成功する状況まで持って行くことが、今回の修正のゴールとなります。

テストケースはJavaScriptのコードだけで完結する場合もありますが、今回のような場合は実際のWebページを使ってテストを行う必要があります。そこで、問題が発生する条件と発生しない条件の両方の事例としてHTMLドキュメントを用意します。

これらのドキュメントを使い、shortPage.htmlではスクロールが発生せずlongPage.htmlではスクロールが発生する、という条件の下でテストを行うテストケースをこれから作成することになります。

ところで、現在の実装で問題が起こっている場合だけでなく、すでに正常に動いている場合の事例も同時に作成していることに気がついたでしょうか? 両方の場合を常にテストすることで、「ある問題を修正したら、今度は、今までは正常に動いていた物が動かなくなった」という状況、いわゆる後退バグを未然に防ぐことができます。 *1

それではテストケースを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
utils.include('pXMigemoClasses.inc.js');

var findModule;

function setUp()
{
  yield utils.setUpTestWindow();
  findModule = new pXMigemoFind();
  findModule.target = utils.getTestWindow().gBrowser;
}

function tearDown()
{
  findModule.destroy();
  findModule = null;
  utils.tearDownTestWindow();
}

function testFindFirstVisibleNode()
{
  // ここに実際のテスト内容を記述する
}

pXMigemoFindクラスの単体テストはまだ作成されていなかったので、今回はsetUpとtearDownから作成します。すでに作成済みのテストケースがあり、それにテスト項目を追加する場合、この作業は不要となります。

pXMigemoFindクラスはtabbrowser要素を用いて初期化する必要があるため、setUpでテスト用のFirefoxウィンドウを開き、そのウィンドウのtabbrowser要素で初期化します。また、tearDownではsetUpで開いたテスト用のFirefoxウィンドウを閉じて、pXMigemoFindクラスのインスタンスを破棄します。UxUは関数名を見て自動的にその種類を判別するため、これだけで、これらの関数はテストの初期化処理と終了処理として認識されるようになります。

なお、インクルードしているpXMigemoClasses.inc.jsというファイルは、pXMigemoFindクラスやそのクラスが依存しているすべての関連クラスの定義を読み込む物です。

次に、テストの内容を作成していきます。

1
2
3
4
5
6
7
8
function testFindFirstVisibleNode()
{
  var win = utils.getTestWindow();
  win.resizeTo(500, 500);
  assert.compare(200, '<', utils.contentWindow.innerHeight);

  // ここに実際のテスト内容を記述する
}

関数名を「test」で始めると、その関数はテストの内容として自動的に認識されます。

最初に、ウィンドウの大きさを調整して、「shortPage.htmlではスクロールが発生せずlongPage.htmlではスクロールが発生する」という条件を整えておきます。ここでは、テスト自体が期待通りの条件下で実行されていることを確認するために、assert.compare()でテスト用フレームの大きさを調べています。

1
2
3
4
5
6
7
8
9
  yield utils.addTab(baseURL+'../res/shortPage.html', { selected : true });

  var frame = utils.contentWindow;
  var node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
  assert.equals(utils.contentDocument.documentElement, node);

  item = frame.document.getElementById('p3');
  node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
  assert.equals(item, node);

テスト用のドキュメントを新しいタブで開き、findFirstVisibleNode()メソッドの返り値が期待通りかどうかを検証します。1つ目の検証は前方検索、2つ目は後方検索です。

同様にして、スクロールが発生する場合のテストも作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  yield utils.addTab(baseURL+'../res/longPage.html', { selected : true });

  frame = utils.contentWindow;
  node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
  assert.equals(utils.contentDocument.documentElement, node);

  item = frame.document.getElementById('p10');
  frame.scrollTo(0, item.offsetTop);
  node = findModule.findFirstVisibleNode(findModule.FIND_DEFAULT, frame);
  assert.equals(item, node);

  frame.scrollTo(0, item.offsetTop - frame.innerHeight + item.offsetHeight);
  node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
  assert.equals(item, node);

  item = frame.document.getElementById('p21');
  frame.scrollTo(0, item.offsetTop - frame.innerHeight + item.offsetHeight);
  node = findModule.findFirstVisibleNode(findModule.FIND_BACK, frame);
  assert.equals(item, node);

スクロールされていない時、ページ途中までスクロールされている時、ページの最後までスクロールされている時の各ケースで前方検索と後方検索を行い、結果を検証します。

ここで、かなりの部分のコードが重複していることに気がついたでしょうか。このような場合、それぞれの検証の前で重複しているコードと検証とをひとまとめにして実行する関数(カスタムアサーション)を定義しておくと、テスト項目の追加が簡単になります。以下は、カスタムアサーションを使ってここまでのテスト内容を書き直した物です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function testFindFirstVisibleNode()
{
  var win = utils.getTestWindow();
  win.resizeTo(500, 500);
  assert.compare(200, '<', utils.contentWindow.innerHeight);

  function assertScrollAndFind(aIdOrNode, aFindFlag)
  {
    var frame = utils.contentWindow;
    var item = typeof aIdOrNode == 'string' ? frame.document.getElementById(aIdOrNode) : aIdOrNode ;
    frame.scrollTo(
      0,
      (aFindFlag & findModule.FIND_BACK ?
        item.offsetTop - frame.innerHeight + item.offsetHeight :
        item.offsetTop
      )
    );
    var node = findModule.findFirstVisibleNode(aFindFlag, frame);
    assert.equals(item, node);
  }

  yield utils.addTab(baseURL+'../res/shortPage.html', { selected : true });
  assertScrollAndFind(utils.contentDocument.documentElement, findModule.FIND_DEFAULT);
  assertScrollAndFind('p3', findModule.FIND_BACK);

  yield utils.addTab(baseURL+'../res/longPage.html', { selected : true });
  assertScrollAndFind(utils.contentDocument.documentElement, findModule.FIND_DEFAULT);
  assertScrollAndFind('p10', findModule.FIND_DEFAULT);
  assertScrollAndFind('p10', findModule.FIND_BACK);
  assertScrollAndFind('p21', findModule.FIND_BACK);
}

テストの実行

テストケースが完成したら、テストを実行してみましょう。実装の修正前なので、当然、このテストは「失敗」という結果が出ます。ですが、この段階では問題ありません。これから、このテストの結果が「成功」になることを目指して実装を修正していきます。

実装の修正

実装の修正内容については省略します。良いアイディアを思いついたら、それを実装に反映して、再度テストを実行してみましょう。テストに成功しないようであれば、まだ修正が必要です。

何度テストを実行しても結果が「成功」になるようになれば、実装の修正はひとまず完了です。修正内容をリポジトリにコミットするなり、修正済みの新しいバージョンとしてリリースするなりしましょう。

新たな問題が発覚した時や、仕様が変わった時は

以上で、今回発見された問題の修正は完了しました。

しかし、上記のテストだけではテストしきれないような、より複雑な条件でだけ発生するバグが新たに見つかるかもしれません。そのような場合は、テストを新たに追加して、それらがすべて「成功」するようになるまで修正してやりましょう。その時はもちろん、他のテストも同時に実行することを忘れないようにしましょう。

また、開発を進める中で、他の部分に加えた変更の影響を受けて上記のテストが失敗するようになることがあるかもしれません。そのような場合、再びテストが通るようになるように実装を修正する必要があります。

実装の仕様を変更した時にも、ここで作成したテストケースは「成功」しなくなる場合があります。このような場合は「ゴール」自体が変わったということになりますので、実装ではなくテストケースの側を修正しなくてはなりません。

まとめ

自動テストを使った開発では、メンテナンスする必要があるコードが「実装」と「テストケース」の2つになるため、一見すると、手間だけが倍増するように思えるかもしれません。

しかし、一連のテスト手順を自動化しておくことで、人の手によるテストでは見落としてしまいかねない思わぬ後退バグの発生に迅速に気づけるようになります。後退バグの発生に日々頭を悩ませている人は、是非、自動テストを開発に取り入れてみてください。

*1  もちろん、後退バグの発生自体は未然には防ぎきれません。しかし、後退バグの発生にすぐ気がつくことができれば、コミットやリリースの前にその後退バグを修正できるため、他の共同開発者やユーザには後退バグの影響を与えずに済むようになります。

タグ: Mozilla | テスト | UxU
2008-11-17

Test::Unit 2.0.1リリース

Test::Unit 2.0.1が RubyForge上でリリースされました。RubyGemsも提供されているの で以下のようにインストールできます。

% sudo gem install test-unit

経緯

Test::UnitはRuby 1.8.xに標準添付されている単体テストフレーム ワークです。しかし、Ruby 1.9.1からは miniunitが標準 添付され、Test::UnitはRubyForgeで開発が継続されることになりま した。これからもTest::Unitを使うときはRubyGemsでインストール することになるでしょう。

Ruby 1.8.xに標準添付されているTest::Unitは互換性のために、 Test::Unit 1.2.3としてリリースされています。Ruby 1.9.1でも Ruby 1.8.xに標準添付されているTest::Unitと同じTest::Unitを使 用したい場合は以下のようにします。

% sudo gem install test-unit --version '= 1.2.3'

テストファイル内(変更前):

1
2
require 'test/unit'
...

テストファイル内(変更後):

1
2
3
4
require 'rubygems'
gem 'test-unit', '= 1.2.3'
require 'test/unit'
...

余談ですが、Ruby 1.9.1でTest::Unitが標準添付から外れ、 miniunitが標準添付になったのはTest::Unitのソースがメンテナン スしづらくなっていたのが主な理由です。

Test::Unit 2.x

Ruby 1.8.xに標準添付されているTest::Unitは長い間メンテナンス はされていましたが、特に機能拡張などは行われていませんでした。 しかし、その間にもテスト環境を便利にするライブラリが公開され てきました。例えば、RSpecのような BDD用のフレームワークや、 expectations のような軽量の単体テストフレームワーク、 Shoulda/ test/spec/ Mochaのように Test::Unitを拡張するライブラリなどです。Test::Unitは少し時代 遅れになってしまったのです。

最近のテスト用のフレームワークは DSL化の方向に向かっているようにも見えます。 これはRSpecの影響が大きいのでしょう。expectationsやShouldaもテスト用の DSLを提供します。

しかし、Test::UnitはDSLを提供しません。テストを「英語らしく」 ではなく「Rubyプログラムらしく」書きます。好みにもよりますが、 これはTest::Unitのメリットの1つと言えます。

Test::Unitに他のフレームワークやライブラリの機能を、 Test::Unitの「Rubyプログラムらしく」テストを書ける特性を活か したまま追加すれば、Test::Unitはもっと便利で使いやすいテスト フレームワークになるでしょう。Test::Unit 2.x系列はRuby 1.8.x に標準添付されていた頃とは違い、そのような方針の元で活発に開 発されていく系列になります。

例えば、以下のような機能が他のフレームワークやライブラリから 移植されています。

  • 差分表示
  • ネストしたテスト定義
  • 色付け
  • C-cでテスト中断時にもテスト結果を表示
  • 複数のsetup/teardown
  • ...

ここでは「差分表示」と「ネストしたテスト定義」だけ紹介します。 *1

差分表示

RSpecでは比較結果が異なった場合に差分を表示して違いをわかり やすく表示してくれます。

diff_spec.rb:

1
2
3
4
5
6
7
8
require 'rubygems'
require 'spec'

describe String do
  it do
    ["I", "am", "a", "boy"].join("\n").should == ["I", "was", "a", "boy"].join("\n")
  end
end

実行結果(差分表示部分のみ):

% ruby diff_spec.rb -D
...
Diff:
@@ -1,5 +1,5 @@
 I
-was
+am
 a
 boy
...

同様の機能がTest::Unit 2.0.1にもあります。

test_diff.rb:

1
2
3
4
5
6
7
8
9
10
require 'rubygems'
gem 'test-unit'
require 'test/unit'

class TestDiff < Test::Unit::TestCase
  def test_diff
    assert_equal(["I", "am", "a", "boy"].join("\n"),
                 ["I", "was", "a", "boy"].join("\n"))
  end
end

実行結果(差分表示部分のみ):

% ruby test_diff.rb
...
diff:
  I
- am
+ was
  a
  boy
...

この例では、ほとんど同じ差分表示ですが、Test::Unit 2.0.1の 差分表示がRSpecの差分表示よりも便利なこともあります。

今度は"\n"ではなく" "でjoinして1行の文字列として比較します。

test_diff.rb:

1
2
3
4
5
6
7
8
9
10
require 'rubygems'
gem 'test-unit'
require 'test/unit'

class TestDiff < Test::Unit::TestCase
  def test_diff
    assert_equal(["I", "am", "a", "boy"].join(" "),
                 ["I", "was", "a", "boy"].join(" "))
  end
end

実行結果(差分表示部分のみ):

% ruby test_diff.rb
...
diff:
- I am a boy
?    ^
+ I was a boy
?   + ^
...

Test::Unit 2.0.1では必要なら同じ行のうち、どの列が異なってい るのかも表示します。RSpecでは行単位の差分までで列単位までの 差分は表示しません。

余談ですが、この差分表示形式はPythondifflib ライブラリで使われている形式です。

ネストしたテスト定義

Shouldaではcontextをネストさせることにより、便利にテストを書 くことができます。以下はShouldaのページからの引用です。 *2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UserTest < Test::Unit::TestCase
  context "A User instance" do
    setup do
      @user = User.find(:first)
    end

    should "return its full name" do
      assert_equal 'John Doe', @user.full_name
    end

    context "with a profile" do
      setup do
        @user.profile = Profile.find(:first)
      end

      should "return true when sent #has_profile?" do
        assert @user.has_profile?
      end
    end
  end
end

ネストされた"with a profile"のcontext内では上位の"A User instance"のcontext内のsetupが実行された後に実行されます。つ まり、以下のような実行順序になります。

  • setup: "A User instance" context
    • should: "return its full name"
  • setup: "A User instance" context
    • setup: "with a profile" context
      • should: "return true when sent #has_profile?"

実行されるフィクスチャ(setup)がネストで自然に表現されていま す。

Test::Unit 2.0.1では以下のように書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class UserTest < Test::Unit::TestCase
  def setup
    @user = User.find(:first)
  end

  def test_full_name
    assert_equal('John Doe', @user.full_name)
  end

  class ProfileTest < UserTest
    def setup
      super
      @user.profile = Profile.find(:first)
    end

    def test_profile
      assert_true(@user.has_profile?)
    end
  end
end

これは以下のように実行されます。

  • UserTest#setup
    • UserTest#test_full_name
  • UserTest#setup
    • UserTest::ProfileTest#setup
      • UserTest::ProfileTest#test_profile

実行されるフィクスチャ(setup)がネストとクラス階層で自然に表現 されています。

まとめ

Test::UnitはRuby 1.9.1からは標準添付ではなくなりましたが、 Test::Unit 2.xとして活発に開発が続けられています。既存の他の フレームワークやライブラリのよいところは積極的に導入している ため、Ruby 1.8.xに標準添付されているTest::Unitよりもはるかに 使いやすくなっています。

今回は紹介しませんでしたが、他のフレームワークやライブラリに はないTest::Unit 2.x独自の便利な機能もあります。Test::Unitの 「Rubyプログラムらしい」テストの書き方が好きな場合はこれから もTest::Unitを使ってみてはいかがでしょうか。

*1  「C-cでテスト中断時にもテスト結果を表示」は開発が進んでい き、たくさんのテストがある場合には地味ですがとても便利な機能 なのです。RSpecにも実装されています。

*2  ただし、Test::UnitはTest::Unit::TestCaseに修正してある。

タグ: Ruby | テスト
2008-11-10

GaUnit 0.1.6リリース

GaUnit 0.1.6がリリースされました。 ([アナウンス]|[ダウンロード])

GaUnitは便利なSchemeインタプリタであるGauche用のxUnitベースの単体テストフレームワークです。0.1.6ではGauche標準のgauche.testモジュール(リンク切れしそうなリンク)用に書かれたテストを実行することができるようになりました。(簡単なものであれば)

gauche.test互換レイヤーの実装

gauche.testのテストを実行するために、テストスクリプトを読み込み、GaUnitが理解できるようにS式を変形します。変形されたS式はGaUnitが提供するgauche.test互換APIが使える無名モジュール内で評価され、GaUnitのテストとして認識されるようになります。

SchemeではマクロでS式を変換するということはよく行われます。もし、それで十分でない場合は上記のように、スクリプト自体をS式として読み込んで、変換して、評価、ということも行うことができます。これは、S式がプログラムで扱いやすいためにできると言えるでしょう。他の言語、例えばJavaScriptでは、プログラムを変換するためにtoSourceしたものをreplaceしてevalするということが行われることもあるようです。

ただ、非常に多くの場合はこのような方法は必要ありません。今回は(use gauche.test)をどうしてもうまく扱えなかったのでこの方式をとっています。

ファイルの内容をS式のリストに変換

Gaucheスクリプトを書くときのちょっとした豆知識を紹介しておきます。

Gaucheスクリプトの内容をS式のリストとして読み込む処理は以下のようになります。ファイル先頭のcoding: utf-8などのような文字エンコーディング指定を考慮するためにopen-coding-aware-portをはさむことを忘れてはいけません。

1
2
3
4
(define (file->sexp-list file)
  (call-with-input-file file
    (lambda (input)
      (port->sexp-list (open-coding-aware-port input)))))

まとめ

GaUnit 0.1.6ではgauche.testのテストも実行できるようになったため、既存のgauche.testのテスト(大事な資産)を捨てることなくGaUnitに移行できるようになりました。これを機にGaUnitも試してみてはいかがでしょうか。

gauche.testであれGaUnitであれ、自動化されたテストがあるということは安心できるものです。

タグ: テスト
2008-10-23

Cutter 1.0.5リリース

ここには書いていませんでしたが、Cutter 1.0.3のリリース のリリースの約1ヶ月後に1.0.4がリリースされました。 さらにその約2ヶ月後の昨日、1.0.5がリリースされました。 CutterとはC言語用の単体テス トフレームワークです。

新機能

詳細は NEWSに 書いてありますが、1.0.5での目玉新機能は以下の3点です。

  • 画像データ入出力・操作ライブラリ gdk-pixbuf (←少し古い。最新版(英語) )をサポート
  • 多くの検証とテスト便利関数を追加
  • ユーザ定義検証作成のサポートを強化

gdk-pixbufサポート

gdk-pixbufをサポートすることにより、画像が等しいかどうかを検 証できるようになりました。また、もし、画像が異なる場合は画像 間の差分を示し、画像のどこが異なるのかをわかりやすくしていま す。

例えば、このような画像を比較したとします。

期待画像 実際の画像

これらの画像では赤い丸の部分が異なっています。

これらの画像の差分画像は以下のようになります。

差分画像

左上に期待画像、右上に実際の画像、左下に異なるピクセルを示し た画像、右下に異なるピクセルを強調表示、同じピクセルを弱めに 表示した画像を配置しています。左下の画像を見ることでどの部分 が異なるのかが具体的にわかり、右下の画像を見ることで比較画像 はどのあたりが異なるのかを相対的に確認する事ができます。

もっとよい表現方法があるかもしれませんが、しばらくはこの方法 を採用しする予定です。もしかすると、今後、よりよい表現方法に 変更されるかもしれません。

ちなみに、この機能はPDF操作・レンダリングライブラリである Popplerテスト で利用されていま す。

検証・テスト便利関数の追加

Cutterを使用していて、こんな検証・便利関数があったら便利だ、 と感じたものは積極的にCutter本体に取り込んでいます。

1.0.5では12個の検証、9個の便利関数が追加されました。追加され た検証・便利関数のリストは NEWSに 書かれています。

ユーザ定義検証作成のサポートを強化

今までもユーザが独自で検証を定義することはできたのですが、バッ クトレースを取得するためにマクロとして定義する必要がありまし た。

1.0.5では cut_trace() というマクロが追加され、検証を関数として定義してもバックトレー スを取得することができるようになりました。マクロは可変長引数 が簡単に書けるなど便利な事も多いのですが、構文エラーが見つけ にくいなどという問題もあります。1.0.5からは関数とマクロを使い 分けられるようになり、より便利にデバッグのしやすいテストが書 けるようになりました。

まとめ

Cutter 1.0.5ではテスト作成・デバッグ支援の機能が強化され、よ り便利なテスト環境を提供するようになりました。今までよりもC 言語でのテスト作成が楽しくなるかもしれません。

[チュートリアル ] [リファレンスマニュアル ] [ダウンロード ]

タグ: Cutter | テスト
2008-10-16

Cutter導入事例: Senna (2)

ちょうど1ヶ月前の話の続きです。

前回でCutterでテストを作成するための環境ができたので、実際にテストを作成していきます。と、思ったのですが、もう一点やらなければいけないことが残っていました。テスト対象のライブラリの初期化についてです。

今回はテスト対象ライブラリの初期化について説明してからテスト作成に入ります。

前回同様、コードの断片がでてきます。完全なものはSennaのリポジトリを見てください。

ライブラリの初期化

Sennaのようにライブラリ初期化・終了関数 (sen_init()/sen_fin())を用意している場合は、テストの作成に 入る前に、もう一つ用意しておかなければいけない仕組みがありま す。このような関数を持っているライブラリをテストする場合は、 テスト全体を実行する前に初期化関数を、実行した後に終了関数を 呼び出す必要があります。これを行う仕組みを用意する必要があり ます。

cutterコマンドは指定されたディレクトリ以下の共有ライブラリを かき集めて、その中からテストを検出して実行します。その時に以 下の条件にあう共有ライブラリを見つけると、テスト全体を実行す る前後に特定の関数を実行することができます。これは、今まさに 必要としている機能です。

  • ファイル名が「suite_」からはじまっている

この共有ライブラリが以下の名前のシンボルを公開している場合は、 その関数をテスト全体を実行する前後に実行します。ここでは、共 有ライブラリのファイル名はsuite_senna_test.soとします。

  • senna_test_warmup(): テスト全体を実行する前に実行
  • senna_test_cooldown(): テスト全体を実行した後に実行

「_warmup」と「_cooldown」の前の「senna_test」の部分は共有ラ イブラリのファイル名から先頭の「suite_」と拡張子を除いた部分 です。

Sennaの場合は以下のようなsuite-senna-test.cを作成します。

test/unit/suite-senna-test.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <senna.h>

void senna_test_warmup(void);
void senna_test_cooldown(void);

void
senna_test_warmup(void)
{
  sen_init();
}

void
senna_test_cooldown(void)
{
  sen_fin();
}

suite-senna-test.cをビルドするためにMakefile.amに以下を追加 します。

test/unit/Makefile.am:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if WITH_CUTTER
...

noinst_LTLIBRARIES =                                \
        suite_senna_test.la
endif

INCLUDES =                        \
        -I$(srcdir)                \
        -I$(top_srcdir)                \
        -I$(top_srcdir)/lib        \
        $(SENNA_INCLUDEDIR)

AM_CFLAGS = $(GCUTTER_CFLAGS)
AM_LDFLAGS = -module -rpath $(libdir) -avoid-version

LIBS =                                                \
        $(top_builddir)/lib/libsenna.la                \
        $(GCUTTER_LIBS)

suite_senna_test_la_SOURCES = suite-senna-test.c

よくあるMakefile.amの書き方です。noinst_LTLIBRARIESをif WITH_CUTTER ... endifの中に入れているのは、Cutterがない環境で はビルド対象からはずし、ビルドエラーにならないようにするため です。

これで、test/unit/.libs/suite_senna_test.soがビルドされるよう になり、テスト全体を実行する前後にSennaの初期化・終了処理を行 うことができます。

テストの作成

テスト実行環境が整ったのでテストを作成します。ここでは検索キー ワードの周辺テキストを取得するsnippet APIのテスト を1つ作成します。

テストの流れは以下の通りです。

  1. sen_snip_open()でsen_snipオブジェクトの生成
  2. sen_snip_add_cond()でキーワードを指定
  3. sen_snip_exec()でsnippetを生成
  4. sen_snip_get_result()で取得した結果が期待していたものか を検証
  5. sen_snip_close()で生成したsen_snipオブジェクトを開放

基本的なCutterのテスト作成方法についてはチュートリアル を参考にしてください。

sen_snip_open()のテスト

まずは、sen_snip_open()でsen_snipオブジェクトを生成する部分 と、sen_snip_close()で生成したsen_snipオブジェクトを開放する 部分を作成します。

今回はGLibサポート付きでCutterを使用するgcutterパッケージを 使っているので、テストは以下のようにgcutter.hを利用します。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <senna.h>
#include <gcutter.h>

void test_simple_exec(void);

static const gchar default_open_tag[] = "[[";
static const gchar default_close_tag[] = "]]";

void
test_simple_exec(void)
{
  sen_snip *snip;

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);
  sen_snip_close(snip);
}

sen_snip_open()は引数が多いですが、ここでは気にする必要はあ りません。sen_snip_open()によりsen_snip *が生成されることだけ知っ ていれば問題ありません。

cut_assert_not_null(snip) でsen_snip *が正常に生成されているかを確認します。これは、 sen_snip_open()は失敗時にはNULLを返すからです。

最後にsen_snip_close()で生成したsen_snip *を開放します。

sen_snip_add_cond()のテスト

次はsen_snip_add_cond()でキーワードを指定する処理を追加しま す。sen_snip_add_cond()の戻り値はsen_rcです。sen_rcはエラー 番号を示す数値でsen_success(0)以外はエラーになります。よって テストは以下のようになります。sen_snip_open()のときと同じく、 sen_snip_add_cond()の引数は気にしなくても構いません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
void
test_simple_exec(void)
{
  sen_snip *snip;
  const gchar keyword[] = "Senna";

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));

  sen_snip_close(snip);
}

sen_snip_add_cond()の結果は cut_assert_equal_int で検証しています。

ただし、ここで問題があります。cut_assert*()は検証が失敗する とその時点でテスト関数からreturnし、それ以降のコードは実行し ません。つまり、cut_assert_equal_int()が失敗した場合は、 sen_snip_open()で生成したsen_snip *が開放されないことになり ます。この問題を解決するためにsetup()/teardown()という仕組み があります。

setup()はテストが実行される前に必ず実行され、teardown()はテ ストが実行された後に成功・失敗に関わらず必ず実行されます。こ の仕組みを利用することで確実にメモリ開放処理を行うことができ ます。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
...
static sen_snip *snip;
...

void
setup(void)
{
  snip = NULL;
}

void
teardown(void)
{
  if (snip) {
    sen_snip_close(snip);
  }
}

void
test_simple_exec(void)
{
  const gchar keyword[] = "Senna";

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));
}

これでcut_assert_equal_int()が成功しても失敗してもsen_snip * は開放されます。Cutterではメモリ開放処理のためにstatic変数と setup()/teardown()を使うことが定石になっています。

sen_snip_exec()のテスト

次はsen_snip_add_cond()で設定したキーワード用のsnippetを生成 するsen_snip_exec()のテストです。sen_snip_exec()もsen_rcを返 すので、それを検証します。また、引数でsnippet数とsnippet文字 列のバイト数も受けとるのでそれも検証します。特に目立った部分 はありません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
static const gchar text[] =
  "Senna is an embeddable fulltext search engine, which you can use in\n"
  "conjunction with various scripting languages and databases. Senna is\n"
  "an inverted index based engine, & combines the best of n-gram\n"
  "indexing and word indexing to achieve fast, precise searches. While\n"
  "senna codebase is rather compact it is scalable enough to handle large\n"
  "amounts of data and queries.";
...
void
test_simple_exec(void)
{
  ...
  unsigned int n_results;
  unsigned int max_tagged_len;

  ...

  cut_assert_equal_int(sen_success,
                       sen_snip_exec(snip, text, strlen(text),
                                     &n_results, &max_tagged_len));
  cut_assert_equal_uint(2, n_results);
  cut_assert_equal_uint(105, max_tagged_len);
}
sen_snip_get_result()のテスト

最後はsen_snip_exec()で生成したsnippetの内容が正しいかどうか のテストです。snippetはsen_snip_get_result()で取得できるので その結果を検証します。n_resultsが2なので2回 sen_snip_get_result()を呼び出す必要があります。

snippetを格納する場所のサイズは動的に決まります。そのため、 snippetを格納する領域を動的に確保する必要があります。 setup()/teardown()の仕組みを用いてメモリを開放するようにしま す。ここ以外は特に目立った部分はありません。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
static gchar *result;

void
setup(void)
{
  ...
  result = NULL;
}

void
teardown(void)
{
  ...
  if (result) {
    g_free(result);
  }
}

void
test_simple_exec(void)
{
  ...
  unsigned int result_len;

  ...
  result = g_new(gchar, max_tagged_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 0, result, &result_len));
  cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, "
                          "which you can use in\n"
                          "conjunction with various scripti",
                          result);
  cut_assert_equal_uint(104, result_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 1, result, &result_len));
  cut_assert_equal_string("ng languages and databases. [[Senna]] is\n"
                          "an inverted index based engine, & combines "
                          "the best of n-gram\ni",
                          result);
  cut_assert_equal_uint(104, result_len);
}

これで単純にsnippet APIを使った場合のテストが1つできました。 同様に、異常な場合や違ったデータを用いた場合などのテストを作 成していきます。

diff

せっかくなのでCutterのテスト結果の出力方法を紹介します。

Cutterは cut_assert_equal_string で文字列の比較が失敗したときには、どの部分が異なったかという 差分情報を表示します。

例えば、今回のテストの最後のcut_assert_equal_string()が失敗 した場合は以下のような差分情報が表示されます。

diff:
  - ng languages and DBes. [[Senna]] is
  ?                  ^^
  + ng languages and databases. [[Senna]] is
  ?                  ^^^^^^^
  - an Inverted Index Based Engine, & combines the best of n-gram
  ?    ^        ^     ^     ^
  + an inverted index based engine, & combines the best of n-gram
  ?    ^        ^     ^     ^
    i

このときの期待した結果は以下の通りです。

ng languages and DBes. [[Senna]] is
an Inverted Index Based Engine, & combines the best of n-gram
i

実際の結果は以下の通りです。

ng languages and databases. [[Senna]] is
an inverted index based engine, & combines the best of n-gram
i

差分を見てもらうと分かる通り、異なっている行を示すだけではな くて、行内で異なっている文字まで示しています。(例えば、DBの 下に^^が付いている。)

広く使われているunified diff形式では行内で異なる文字は表示し ません。テストでは1行のみの比較を行うことも多く、行単位だけ の差分よりも文字単位での差分表示も行った方がデバッグが行いや すいという判断からこのような形式になっています。

この形式はPythonのdifflibにあるndiffの形式と同じものです。

テスト全体

今回作成したテストは以下の通りです。

test/unit/test-snip.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <senna.h>
#include <gcutter.h>

void test_simple_exec(void);

static sen_snip *snip;
static const gchar default_open_tag[] = "[[";
static const gchar default_close_tag[] = "]]";
static const gchar text[] =
  "Senna is an embeddable fulltext search engine, which you can use in\n"
  "conjunction with various scripting languages and databases. Senna is\n"
  "an inverted index based engine, & combines the best of n-gram\n"
  "indexing and word indexing to achieve fast, precise searches. While\n"
  "senna codebase is rather compact it is scalable enough to handle large\n"
  "amounts of data and queries.";
static gchar *result;

void
setup(void)
{
  snip = NULL;
  result = NULL;
}

void
teardown(void)
{
  if (snip) {
    sen_snip_close(snip);
  }
  if (result) {
    g_free(result);
  }
}

void
test_simple_exec(void)
{
  const gchar keyword[] = "Senna";
  unsigned int n_results;
  unsigned int max_tagged_len;
  unsigned int result_len;

  snip = sen_snip_open(sen_enc_default, 0, 100, 10,
                       default_open_tag, strlen(default_open_tag),
                       default_close_tag, strlen(default_close_tag),
                       NULL);
  cut_assert_not_null(snip);

  cut_assert_equal_int(sen_success,
                       sen_snip_add_cond(snip, keyword, strlen(keyword),
                                         NULL, 0, NULL, 0));

  cut_assert_equal_int(sen_success,
                       sen_snip_exec(snip, text, strlen(text),
                                     &n_results, &max_tagged_len));
  cut_assert_equal_uint(2, n_results);
  cut_assert_equal_uint(105, max_tagged_len);

  result = g_new(gchar, max_tagged_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 0, result, &result_len));
  cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, "
                          "which you can use in\n"
                          "conjunction with various scripti",
                          result);
  cut_assert_equal_uint(104, result_len);

  cut_assert_equal_int(sen_success,
                       sen_snip_get_result(snip, 1, result, &result_len));
  cut_assert_equal_string("ng languages and databases. [[Senna]] is\n"
                          "an inverted index based engine, & combines "
                          "the best of n-gram\ni",
                          result);
  cut_assert_equal_uint(104, result_len);
}

問題発生時に有用なデバッグ情報を増やしたり、より読みやすいテ ストにするなど、いろいろ改良するべき点は残っていますが、今回 はこれで終了します。実際のコードはSennaのリポジトリを参照 してください。

まとめ

2回に分けて以下のことについて説明しました。

  • GNUビルドシステムを採用した既存のプロジェクトへのCutter の組み込み方法
    • cutter.m4で提供するM4マクロの使用方法
    • Cutterをインストールしていないユーザへの対応
    • Cutterをインストールしていない開発者への対応
  • Cutterを用いたテスト環境の構築方法
    • 便利なテスト起動スクリプトrun-test.shの作成方法
    • 初期化・終了関数があるライブラリのテスト方法
  • Cutterを用いたテストの作成方法
    • setup()/teardown()を用いたメモリ管理の方法
    • diffの出力

Cで書かれたプロジェクトに単体テストフレームワークを導入する 場合はCutterも検討してみてはいかがでしょうか。

つづき: 2012-12-12
タグ: Cutter | テスト
2008-08-25

Cutter導入事例: Senna (1)

Sennaの単体テストフレームワー クとしてCutterを導入したときの手順です。自分のプロジェクトに Cutterを導入するときの参考になるかもしれません。全体として そこそこ長くなってしまったので、何回かに分割して紹介することに します。

内容はSennaのリポジトリ でやったことの一部です。リポジトリは公開されているので、試行錯誤の 後などをみたい場合はコミットを追いかけるとよいでしょう。また、ここで は断片としてしか出てこないコードについても、リポジトリの中には完全な 形で入っています。

もし、まだCutterについて知らない場合は、はじめにチュートリ アル を読んでください。

はじめに

まず、Sennaについて簡単に説明します。

Sennaは組み込み型の全文検索エンジンで、その機能をライブラリ として提供します。SennaのAPIはbasic APIやadvanced APIなどい くつかのグループにわかれています。

今回はSennaの単体テストフレームワークとしてCutterを導入し、 utility APIのひとつ、snippet*1のテストを 作成するまでを示します。このためには以下の作業が必要になりま す。

  • SennaのビルドシステムにCutterを組み込む
  • Cutterでsnippet APIのテストを記述する

作業に入る前にSennaのビルドシステムについて確認します。

Sennaのビルドシステム

SennaではGNU AutomakeGNU Libtoolな どGNUビルドシステムを利用したビルドシステムを採用しています。

CutterはGNUビルドシステムサポート用の機能をいくつか提供してい ます。そのため、GNUビルドシステムを用いているプロジェクトへ はCutterを容易に導入することができます。

もし、これからプロジェクトを始める場合でGNUビルドシステムを 採用する場合はCutterのチュートリアル が参考になるでしょう。

ビルドシステムへのCutterの組み込み

Sennaの単体テストフレームワークとしてCutterを採用するにあたっ て、以下のような条件を満たすこととします。

  • Cutterがない場合でもユーザがSennaをビルドできること
  • Cutterがない場合でも開発者がSennaをビルドできること
  • configure時にCutterを使用するかどうかを指定できること
  • Cutterで作成したテストはtest/unit/以下に配置すること
  • テストではテストを簡潔・容易に記述するためにGLibを利用す る

上記の中でのユーザと開発者の違いは、autogen.shを用いて自分で configureを作成するかどうかです。ユーザは開発者が作成した configureを利用するため、自分でconfigureを作成しません。一方、 開発者はSubversionリポジトリ内にはconfigureは入っていないの でautogen.shを使ってconfigure.acからconfigureを作成し、利用 します。つまり、違いは以下の通りになります。

  • ユーザ: configureのみ実行
  • 開発者: autogen.shとconfigureを実行

それでは、まずは、開発者はすべてCutterをインストールしている ものとしてCutter対応のconfigureを生成できるようにします。

cutter.m4

Cutterはconfigure.ac内で利用できるCutter検出用のM4マクロを cutter.m4として提供しています。このファイルは ${PREFIX}/share/aclocal/cutter.m4としてインストールされます。 ${PREFIX}/share/aclocal/以下に他の.m4ファイルがインストールされ ているような環境ではおそらくそのままで大丈夫ですが、そうでな い場合はautogen.shの中でaclocalを呼び出しているところを編集 して${PREFIX}/share/aclocal/以下を.m4ファイルの検索パスに加 える必要があります。

もし、Cutterのconfigureに--prefix=/tmp/localオプションをつけ てビルド・インストールした場合はautogen.shを以下のように変更 する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
Index: autogen.sh
===================================================================
--- autogen.sh        (リビジョン 820)
+++ autogen.sh        (作業コピー)
@@ -105,7 +105,7 @@
 echo "Running libtoolize ..."
 $LIBTOOLIZE --force --copy
 echo "Running aclocal ..."
-$ACLOCAL ${ACLOCAL_ARGS} -I .
+$ACLOCAL ${ACLOCAL_ARGS} -I . -I /tmp/local/share/aclocal
 echo "Running autoheader..."
 $AUTOHEADER
 echo "Running automake ..."

あるいはautogen.shを実行する時に環境変数ACLOCAL_ARGSを指定し ます。

% ACLOCAL_ARGS="-I /tmp/local/share/aclocal" ./autogen.sh

これでconfigure.ac内でCutterが提供する便利M4マクロを利用する 準備が整いました。

テスト作成用パッケージ

Cutterはパッケージを pkg-configのパッ ケージとしてインストールします。パッケージをpkg-configのパッ ケージとして作成しているのは、pkg-configが広く普及していて、 GNUビルドツールなどpkg-configに対応しているビルドシステムが 多いからです。

Cutterは、テスト作成用に以下の2つのパッケージを用意しています。

  • cutter: Cutterを利用してテストを作成する場合に利用
  • gcutter: cutterパッケージにGLibサポート機能を追加したパッ ケージ。GLibを利用してもっと簡潔・容易にテストを書きたい 場合に利用

今回はGLibを利用してテストを作成するので、cutterパッケージで はなくgcutterパッケージを利用します。

Cutterはconfigure.acで簡単にcutter/gcutterパッケージの設定を 行えるように以下のM4マクロを提供しています。

AC_CHECK_CUTTER

cutterパッケージ検出マクロです。以下の変数をAC_SUBSTしま す。

  • CUTTER: cutterコマンドのパス
  • CUTTER_CFLAGS: cutterパッケージを用いたテストをビルド するためのCFLAGS
  • CUTTER_LIBS: cutterパッケージを用いたテストをビルド するためのLIBS

また、cutterパッケージが利用不可能な場合は ac_cv_use_cutterが"no"になります。

AC_CHECK_GCUTTER
gcutterパッケージ検出マクロです。上述のAC_CHECK_CUTTERマ クロがAC_SUBSTする変数に加えて、以下の変数もAC_SUBSTしま す。
  • GCUTTER_CFLAGS: gcutterパッケージを用いたテストをビル ドするためのCFLAGS
  • GCUTTER_LIBS: gcutterパッケージを用いたテストをビルド するためのLIBS

今回はGLibサポートがついたgcutterパッケージを利用するので、 AC_CHECK_GCUTTERマクロを利用します。よってconfigure.acには以 下を追加することになります。

configure.ac:

1
2
3
4
5
6
AC_CHECK_GCUTTER

AM_CONDITIONAL([WITH_CUTTER], [test "$ac_cv_use_cutter" != "no"])
if test "$ac_cv_use_cutter" != "no"; then
  AC_DEFINE(WITH_CUTTER, 1, [Define to 1 if you use Cutter])
fi

これで、Makefile.amではCutterが利用できるかどうかはif WITH_CUTTER ... endifで判断できます。Makefile.amではCutterが 利用できない場合はテストプログラムをビルドしないようにします。 こうすることにより、ユーザがCutterをインストールしていなくて も、Sennaをビルドできます。

cutter.m4がない場合への対応

cutter.m4がない場合は./autogen.shの実行が失敗します。つまり、 開発者がconfigureを正常に生成できなくなります。

残念ながら、Cutterはそれほど有名なフリーソフトウェアではない ため、開発者がCutterをインストールしていることはほとんどあり ません。そこで、開発者がCutterをインストールしていなくても configureを生成できるようにします。*2

cutter.m4がインストールされているかどうかはAC_CHECK_GCUTTER 関数が定義されているかどうかでわかります。そのため、以下のよ うに書くことにより、Cutterがインストールされてない環境でも configureを生成できます。もちろん、生成されたconfigureには Cutterの検出機能などはありません。

configure.ac:

1
2
3
4
5
6
7
8
9
m4_ifdef([AC_CHECK_GCUTTER], [
AC_CHECK_GCUTTER
],
[ac_cv_use_cutter="no"])

AM_CONDITIONAL([WITH_CUTTER], [test "$ac_cv_use_cutter" != "no"])
if test "$ac_cv_use_cutter" != "no"; then
  AC_DEFINE(WITH_CUTTER, 1, [Define to 1 if you use Cutter])
fi

このようにAC_CHECK_GCUTTERの呼び出し部分をm4_ifdefの中に入れ るだけです。AC_CHECK_GCUTTERが定義されていない場合は ac_cv_use_cutterを"no"にしているのでWITH_CUTTERが真になるこ とはありません。

ビルドシステムへtest/unit/以下を追加

Cutterを用いたテストプログラムはtest/unit/以下に配置します。 このディレクトリは新規に作成するため、以下の作業が必要になり ます。

  • test/Makefile.amのSUBDIRSにunitを追加
  • configureでtest/unit/Makefileを生成する設定を追加
  • test/unit/Makefile.amの作成

まずは、test/Makefile.amのSUBDIRSにunitを追加し、test/unit/ 以下もビルド対象とします。

test/Makefile.am:

1
SUBDIRS = unit

続いて、configure.acのAC_CONFIG_FILESにtest/unit/Makefileを 追加し、configureがtest/unit/Makefileを生成するようにします。

configure.ac:

1
AC_CONFIG_FILES([... test/unit/Makefile ...])

最後に、test/unit/Makefile.amを作成し、test/unit/以下のビル ド方法を設定します。とりあえず、今は空っぽでかまいません。

% touch test/unit/Makefile.am

これで、test/unit/以下をSennaのビルドシステムに加えることがで きました。再度./autogen.sh, ./configureを実行してからmakeす れば、test/unit/以下もビルド対象になっていることがわかります。

% ./autogen.sh
% ./configure
% make
...
make[3]: ディレクトリ `.../test/unit' に入ります
...
テスト起動コマンド

test/unit/以下がビルド対象に加わったので、test/unit/以下に作 成するテストプログラムを起動するコマンドを作成します。このテ スト起動コマンドはmake checkから呼び出されることになります。

テスト起動コマンドは伝統的にrun-test.shというシェルスクリプ トになっています。このシェルスクリプトからcutterコマンドを呼 び出してテストを実行します。

cutterを実行するときはいくつかオプションを指定する必要があり ます。例えば、テストプログラムがあるディレクトリなどがそれで す。ここでrun-test.shを作成する理由は、cutterへ渡すオプション などを指定しなくてもよいようにするなど、より簡単にテストを実 行できるようにするためです。

テストが簡単に実行できるということはとても重要なことです。テ ストを実行することが面倒だと、だんだんテストを実行しなくなっ てしまうからです。テストが実行されないと、新しくテストを作成 することも面倒になってくるでしょう。これは悪い循環といえます。 これを防ぐためにも最初のうちから簡単にテストを実行できる仕組 みを用意しておくことが重要です。

また、引数なしでも動くrun-test.shを用意することにはもう一つ理 由があります。それは、GNU Automakeが提供するテスト起動の仕組 みであるmake checkからも利用できるようにすることです。make checkでは指定されたテスト起動スクリプトが引数なしでテストを実 行できる必要があります。*3

前置きが長くなりましたがテストをもっと簡単に走らせるためのス クリプト、run-test.shは以下のようになります。

test/unit/run-test.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

export BASE_DIR="`dirname $0`"

if test x"$NO_MAKE" != x"yes"; then
    make -C $BASE_DIR/../../ > /dev/null || exit 1
fi

if test -z "$CUTTER"; then
    CUTTER="`make -s -C $BASE_DIR echo-cutter`"
fi

if test x"$CUTTER_DEBUG" = x"yes"; then
    CUTTER="$BASE_DIR/../../libtool --mode=execute gdb --args $CUTTER"
fi

CUTTER_ARGS="-s $BASE_DIR"
$CUTTER $CUTTER_ARGS "$@" $BASE_DIR

このスクリプトではmake check以外からも便利に利用できるように なっています。make check以外から起動された場合(つまり直接 test/unit/run-test.shを軌道した場合)は必要なビルドを行ってか らテストを起動します。つまり、run-test.shからテストを起動した 場合はビルド忘れがなくなります。

実は、上記のrun-test.shを直接起動できるようにするためには、 test/unit/Makefile.amにも一工夫する必要があります。それは、 configureで検出したcutterコマンドのパスをrun-test.shに伝える ためのターゲットを用意するということです。

test/unit/Makefile.am:

1
2
echo-cutter:
        @echo $(CUTTER)

これで、run-test.shを直接起動しても、必要に応じてビルドした り、情報を集めたりしてテストを起動してくれます。

また、make checkではテスト結果とビルド結果が混ざりそこそこの 出力になりますが、run-test.sh経由でビルド・テストを行うと必 要最小限の出力になり、問題の発見が簡単になります。実際の開発 は以下のようなサイクルになります。

  1. ソース変更
  2. test/unit/run-test.shを実行

    テスト失敗→(1)に戻る

  3. コミット→(1)に戻る

手順が少ないため開発のリズムが崩れにくくなります。このサイク ルをより簡単に行うための方法もあるのですが、それはまた別の機 会にします。

run-test.shができたので、make checkでrun-test.shを起動するよ うにMakefile.amを変更します。

test/unit/Makefile.am:

1
2
3
4
5
if WITH_CUTTER
TESTS = run-test.sh
TESTS_ENVIRONMENT = NO_MAKE=yes
...
endif

TESTS_ENVIRONMENTにNO_MAKE=yesを指定することにより、make check経由の場合はテスト実行前のmake実行を抑制します。

これでテストを実行するための環境は整いました。きりがよいので 今回はここまでにします。

まとめ

ここまでで、以下のことについて説明しました。

  • GNUビルドシステムを採用した既存のプロジェクトへのCutter の組み込み方法
    • cutter.m4で提供するM4マクロの使用方法
    • Cutterをインストールしていないユーザへの対応
    • Cutterをインストールしていない開発者への対応
  • Cutterを用いたテスト環境の構築方法
    • 便利なテスト起動スクリプトrun-test.shの作成方法

続きではテストを作成します。

*1  検索キーワードの周辺テキストの こと。ここではそれを取得するSennaの機能のこと。

*2  本当は開発者には頻繁に テストを走らせて欲しいのでCutterを必須にしたいところです。

*3  テスト起動スクリプトにオプションを 指定する場合は環境変数を利用します。

タグ: Cutter | テスト
2008-07-25

Cutter 1.0.3リリース

昨日、C言語用の単体テストフレームワークである Cutterの1.0.3がリリースされま した。

実は、Cutter-1.0リリースから3回リリースしていま す。1.0.0以降はマイクロバージョンだけを上げていますが、新しく 追加された機能はマイクロとは思えません。例えば、Windows (MinGW)でのビルド に対応、GStreamer のサポートなどといった機能が含まれていました。過去のリリースに ついてはNEWS を見てください。

Cutterとは

Cutterはテストの書きやすさ・テスト結果からのデバッグのしやす さを重視したC言語用の単体テストフレームワークです。今回のリリー スからCutterの機能を説明したページ を用意 しました。

データ駆動テスト対応

同じテストを条件を変えて実行したい時があります。例えば、以下 のような場合です。

  • 複数の入力パターンがあり、それらを網羅的にテストする場合
  • 複数のバックエンドを抽象化し、どのバックエンドを利用して いる場合でも同じインターフェイスで扱えるライブラリをテス トする場合(Cでのcairo、Perl/Ruby/GaucheなどでのDBI、 RubyでのActiveRecordなど)

このような場合、必要な分だけテストコードをコピー&ペーストして テストを作成するよりも、以下のように書けるとテスト記述・管理 のコストを下げることができます。

  • テストは1つだけ用意
  • テスト条件、つまり、入力データを複数用意
  • 各入力データに対してそれぞれテストを実行

このようなテストの方法をデータ駆動テストと呼びます。

データ駆動テストではデータの用意の仕方にはいくつかの方法があ り、それぞれ利点があります。

データベースに保存された入力データを利用
大量のデータを用意したり、データを一括変更できるなどデー タ管理機能が豊富
CSVなど表形式の入力データを利用
Excelなどを利用して入力データを用意することができる
プログラム内で入力データを生成
動的にデータを用意するので、柔軟にデータを生成すること ができる。例えば、文字'a', 'b', 'c'を使って作られる長さ が3の文字列すべて("abc", "acb", ...)、などというデータ を用意できる。

Cutterでは今回のリリースで、最後の「プログラム内で入力データ を生成」する方法をサポートしました。使い方は以下の通りです。

  • data_XXX(void)を定義
  • data_XXX()中でcut_add_data()を使ってデータを登録
  • test_XXX(const void *data)を定義
    • dataにはcut_add_data()で登録したデータの1つが渡る

今までどおり、関数を定義するだけでよく、他のC言語用の単体テ ストフレームワークにあるような「登録処理」のようなことは必要 ありません。Cutterが自動で見つけてくれます。

コードにすると以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
data_XXX(void)
{
    cut_add_data("データ1の名前", data1, data1_free_function,
                 "データ2の名前", data2, data2_free_function,
                 "データの例", strdup("test data"), free,
                 ...)
}

void
test_XXX(const void *data)
{
    /* dataはdata_XXX()で登録した「data1」か「data2」
       か「strdup("test data")」。test_XXX()はそれぞれに対
       して1回ずつ、計3回呼ばれる。
     */
    cut_assert_equal_string("test data", data);
}

具体例は cut_add_data() を見てください。

まとめ

Cutter 1.0.3ではデータ駆動テストをサポートし、より簡単にテス トがかけるようになりました。

つづき: 2008-10-16
タグ: Cutter | テスト
2008-07-16

Pikzie 0.9.2リリース

Python用の単体テストフレームワークである Pikzie 0.9.2がリリースされま した。

以下のようにeasy_installでインストールできます。

% sudo easy_install Pikzie

類似のテスティングフレームワーク

Python用の単体テストフレームワークとしてはPythonに標準添付さ れている unittest や、unittestよりも柔軟にテストが書ける py.testなど があります。

また、unittest自体を拡張してより柔軟にテストが書けるようにし た nose もあります。noseはプラグイン方式をサポートしており、柔軟にテ ストが書ける機能以外にも、プラグインとして以下のような機能を 提供しています。

  • テストを省略するskipの追加
  • カバレッジをレポートする機能
  • doctest のサポート
  • ...

また、BDD用のテスティングフレームワークとしては pyspecがあります。

Pikzieを使う理由

上記に挙げた既存の単体テストフレームワークには共通して以下の ような問題点があります*1

  • 期待値(expected)と実測値(actual)の違いを判断することが困 難な出力結果

上記の問題を解決することがPikzieを使うもっとも大きな理由にな ります。これは、テストの失敗結果を使いながらデバッグすること が多いからです。

テストを書くことの重要性、テストの書き方*2などを解説しているものはよくみますが、 テストが失敗してそれを修正していく過程を書いているものはなか なかみかけません。しかし、テストは一度成功したらそれ以降も成 功し続けるわけではないのです。開発の途中でテストは何度も何度 も失敗します。例えば、以下のような場合に既存のテストが失敗す るかもしれません。

  • 新しい機能を追加するとき
  • 既存の機能を改良せずにパフォーマンスを改良するとき
  • 仕様が変わったとき
  • 仕様を変更せずに実装を修正するとき
  • テスト用データを追加・変更したとき
  • 依存しているライブラリのバージョンがあがったとき

つまり、新しいテスト・機能を開発していくときに既存のテストが 失敗することは当たり前のことです。

テスト結果の表示

以下はunittestで書かれたテストです。

1
2
3
4
5
6
7
8
9
10
# unittest-test.py
import unittest

class FailTestCase(unittest.TestCase):
    def test_fail(self):
        x = 1110111011110
        self.assertEquals(x + 100000, 1111111011111)

if __name__ == '__main__':
    unittest.main()

このテストの実行結果は以下のようになります。

% python unittest-test.py
F
======================================================================
FAIL: test_fail (__main__.FailTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "unittest-test.py", line 6, in test_fail
    self.assertEquals(x + 100000, 1111111011111)
AssertionError: 1110111111110 != 1111111011111

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

期待値と実測値は以下のように表示されています。

AssertionError: 1110111111110 != 1111111011111

どこが違うのかがわかりにくいのがわかると思います。

noseを用いた場合もテストの書き方は変わりません。ただし、テス トを実行するためにnosetestsコマンドを使う必要があります。

% nosetests unittest-test.py
F
======================================================================
FAIL: test_fail (unittest-test.FailTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/unittest-test.py", line 6, in test_fail
    self.assertEquals(x + 100000, 1111111011111)
AssertionError: 1110111111110 != 1111111011111

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

以下はpy.testで書いたテストです。

1
2
3
4
5
6
# pytest-test.py
import py

def test_fail():
    x = 1110111011110
    assert x + 100000 == 1111111011111

このテストの実行結果は以下のようになります。py.testコマンド で起動します。

% /tmp/py-0.9.1/py/bin/py.test pytest-test.py
inserting into sys.path: /tmp/py-0.9.1
============================= test process starts =============================
executable:   /usr/bin/python  (2.5.2-final-0)
using py lib: /tmp/py-0.9.1/py <rev unknown>

pytest-test.py[1] F

_______________________________________________________________________________
____________________________ entrypoint: test_fail ____________________________

    def test_fail():
        x = 1110111011110
E       assert x + 100000 == 1111111011111
>       assert (1110111011110 + 100000) == 1111111011111

[/tmp/pytest-test.py:5]
_______________________________________________________________________________
================== tests finished: 1 failed in 0.08 seconds ===================

unittestと違ってxの値が展開されていますが、 (1110111011110 + 100000)の結果は表示されていません。こ のため、実際の値が期待値とどう違うのかがわかりません。

最後にPikzieで書いたテストです。

1
2
3
4
5
6
7
# pikzie-test.py
import pikzie

class TestFail(pikzie.TestCase):
    def test_fail(self):
        x = 1110111011110
        self.assert_equal(1111111011111, x + 100000)

実行結果は以下の通りです。特別なテスト起動コマンドは必要あり ません。

% python pikzie-test.py
F

1) Failure: TestFail.test_fail: self.assert_equal(1111111011111, x + 100000)
pikzie-test.py:6: self.assert_equal(1111111011111, x + 100000)
expected: <1111111011111>
 but was: <1110111111110>

Finished in 0.005 seconds

1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 notification(s)

以下のように期待値と実測値が並べて表示されるので、違いをみつ けやすくなります。

expected: <1111111011111>
 but was: <1110111111110>

また、場合によってはdiffが表示されます。

1
2
3
4
5
6
# pikzie-test-diff.py
import pikzie

class TestDiff(pikzie.TestCase):
    def test_diff(self):
        self.assert_equal("aaaaaxaaaaaaaaa", "aaaaaoaaaaaaaaa")

実行結果です。

% python pikzie-test-diff.py
F

1) Failure: TestDiff.test_diff: self.assert_equal("aaaaaxaaaaaaaaa", "aaaaaoaaaaaaaaa")
pikzie-test-diff.py:5: self.assert_equal("aaaaaxaaaaaaaaa", "aaaaaoaaaaaaaaa")
expected: <'aaaaaxaaaaaaaaa'>
 but was: <'aaaaaoaaaaaaaaa'>

diff:
- aaaaaxaaaaaaaaa
?      ^
+ aaaaaoaaaaaaaaa
?      ^

Finished in 0.033 seconds

1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 notification(s)

また、長い行の場合は折り返してdiffを表示します。

1
2
3
4
5
6
7
8
9
10
11
# pikzie-test-diff-long
import pikzie

class TestDiffLong(pikzie.TestCase):
    def test_diff_long(self):
        self.assert_equal("ppppppppppppppppppppppyyyyyyyyyyyyyyy"
                          "ttttttttttttttttttttttttttttttttttttt"
                          "hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhon",
                          "ppppppppppppppppppppppyyyyyyyyyyyyyyy"
                          "ttttttttttttttttttttttttttttttttttttt"
                          "hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoon")

実行結果です。

% python pikzie-test-diff-long.py
F

1) Failure: TestDiffLong.test_diff_long: "ppppppppppppppppppppppyyyyyyyyyyyyyyy"
pikzie-test-diff-long.py:8: "ppppppppppppppppppppppyyyyyyyyyyyyyyy"
expected: <'ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhon'>
 but was: <'ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoon'>

diff:
- ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhon
?                                                                                                             ^
+ ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoon
?                                                                                                             ^

folded diff:
  ppppppppppppppppppppppyyyyyyyyyyyyyyyttttttttttttttttttttttttttttttttttttthhhh
- hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhon
? -
+ hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoon
?                                +

Finished in 0.008 seconds

1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 notification(s)

diffの後に、長い行を折り返した結果に対するdiffも表示されてい ます。期待値・実測値が長い文字列で表現される場合は、折り返し た結果のdiffを見た方が異なる部分を見つけやすくなります。

まとめ

テストは頻繁に失敗します。Pikzieはテストの修正に必要な情報を できるだけ多く、簡潔に表示します。これは、テストの修正を迅速 に行うために大事なことです。

ここでは書きませんでしたが、もちろん、Pikzieは他のテスティン グフレームワークと同じように柔軟にテストを書くことができます。

*1  unittestは命名規則がCamelCaseで PEP 8 -- Style Guide for Python Codeから外れ ているという問題もあります。

*2  例えば、テストの粒 度やテスト駆動開発など

タグ: テスト
2008-06-27

UxUで始めるFirefoxアドオンの自動テスト

Firefox用アドオンやXULRunnerアプリケーションなどのいわゆるXULアプリケーションは、ロジック部を主にJavaScriptで記述するため、script.aculo.usのテスト関連機能などJavaScript用のテストツールを使って自動テストを行えます。しかし、一般的なJavaScript用のテストツールはWebアプリケーションをテストすることを主眼において開発されているため、利用できる機能に制限があったり、HTMLではなくXULを使用するXULアプリケーションのテストでは不具合が生じたりする場合があります。

UxU(UnitTest.XUL)は、著名なXULアプリケーション開発支援ツールであるMozLabをベースにクリアコードで開発を行っている自動テスト実行ツールです。FirefoxやThunderbirdなどのXULアプリケーション上での利用を前提としているため、前述のような制限や問題を気にすることなく自動テストを記述できる、便利なヘルパーメソッドを利用できる、などの特長があります。

テストの記述方法やヘルパーメソッドの一覧はUxUの紹介ページに情報がありますが、ここではFirefox用アドオンのXUL/Migemoのテストを実例として示しながら、UxUによる自動テストの方法について簡単にご紹介をしたいと思います。Subversionのリポジトリ内にテストのサンプル用に用意されたタグがありますので、まずはこちらから必要なファイル一式をチェックアウトしておいてください。

クラスのユニットテスト

まずは最も簡単な例として、「tests」→「unit」とフォルダを辿った中にあるdocShellIterator.test.jsを見てみましょう。

XUL/MigemoはFirefoxのページ内検索を拡張するアドオンですので、フレーム内に検索語句が見つからなかったときは、子フレームや親フレームを検索対象として自動的に再検索を行うといった処理が必要になります。docShellIterator.test.jsでは、このための処理を担当するクラス「DocShellIterator」が正しく機能するかどうかをテストしています。

include
1
utils.include('../../components/pXMigemoFind.js', null, 'Shift_JIS');

冒頭では、ヘルパーメソッドのutils.includeを使って、DocShellIteratorクラスが定義されているファイルpXMigemoFind.jsの内容を取り込んでいます。第3引数で読み込むファイルのエンコーディングを指定していますが、これはファイルの中に含まれる日本語のコメントがShift_JISになっているためです。

なお、何らかの事情でそのままファイルをincludeできない(includeするとまずい)場合には、ファイルの内容をテキストとして読み込んで加工した後に評価するという方法もあります。上の例は、以下のように書き換えても同様に動作します。

1
2
3
4
5
var extract;
eval('extract = function() { '+
     utils.readFrom('../../components/pXMigemoFind.js') +
     '; return DocShellIterator }');
var DocShellIterator = extract();

utils.readFromはファイルの内容を文字列として返しますので、replaceなどを使って邪魔な部分を消してやれば、そのままではincludeできないファイルから必要な部分だけを取り出して評価できます。

テストケースの定義

このファイルにはテストケースが一つだけ定義されています。テストの前処理(setUp)と後処理(tearDown)は以下の通りです。

1
2
3
4
5
6
7
8
9
10
11
12
var DocShellIteratorTest = new TestCase('DocShellIteratorのユニットテスト');

DocShellIteratorTest.tests = {

  setUp : function() {
    yield Do(utils.loadURI('../res/frameTest.html'));
  },

  tearDown : function() {
    iterator.destroy();
    yield Do(utils.loadURI());
  },

setUpとtearDownは、それぞれのテストを実行する前と後に毎回実行されます。このテストケースでは、テスト実行前に「テストに使用するフレームにHTMLファイルを読み込む」という処理を行い、テスト終了後に「フレームに空のページを読み込んで内容を破棄する」という処理を行うことで、毎回必ずクリーンなテスト環境を準備するようにしています。

setUpの中では、「フレームに指定したページを読み込み、読み込みが完了するのを待って次に進む」といった処理待ちを行うために、ヘルパーメソッドとyield式を使用しています。yieldは本来はJavaScript 1.7で導入されたジェネレータのための物ですが、UxUではこれを応用して処理待ちを実現しています。JavaScriptで処理待ちというと、タイマーやonloadのようなイベントハンドラを使う方法が真っ先に思い浮かぶと思いますが、yield式を使用すれば、それらの場合に比べて処理の流れをフラットに記述することができます。

テストの内容

個々のテストの定義を見てみましょう。以下のテストでは、DocShellIteratorクラスのインスタンスを生成して、検索対象のフレームを移動する処理を実際に行い、処理結果が期待されたものと等しいかどうかをテストしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'前方検索': function() {
  // 1番目のフレームを初期状態としてインスタンス生成。
  iterator = new DocShellIterator(content.frames[0], false);
  // 初期化は成功したか?
  assert.initialized(iterator, content.frames[0]);

  // 次のフレームに移動するメソッドを実行。
  assert.isTrue(iterator.iterateNext());
  // フォーカスは正常に2番目のフレームに移動したか?
  assert.focus(iterator, content.frames[1]);
  assert.isFalse(iterator.isInitial);
  // もう一度フレームを移動。
  // 3番目のフレームは無いので、一巡して最上位のフレームにフォーカスする。
  assert.isTrue(iterator.iterateNext());
  // フォーカスは正常に最上位のフレームに移動したか?
  assert.focus(iterator, content);
  assert.isFalse(iterator.isInitial);
  // もう一度フレームを移動。1番目のフレームに戻る。
  assert.isTrue(iterator.iterateNext());
  // フォーカスは正常に1番目のフレームに移動したか?
  assert.focus(iterator, content.frames[0]);
},

処理が成功したかどうかを確認する手続きを、アサーション(宣言)と呼びます。assert.isTrue、assert.isFalse、assert.equalsなどのアサーション用ヘルパーメソッドは、実行されると渡された値を評価します。実際に渡された値が期待された値と等しければそのまま処理を続行しますが、値が異なっていた場合は「アサーションに失敗した」という内容の例外を発生させてテストの実行が中断されます。これらの例外やメソッド実行時に発生した未知の例外はUxUのインターフェース上に逐次表示され、例外が発生した行のスタックトレースを後で辿ることができるため、どの時点で問題が起こったのか、どこまでは予想通りに正常に動いたのかを詳しく調べてデバッグに役立てられます。

UxUのページのアサーション一覧にないassert.initializedやassert.focusはこのテスト専用に定義したカスタムアサーションです。その実体は、このファイルの冒頭でassertオブジェクトに追加されている新しいメソッドで、以下のように、内部でより単純なアサーションを複数行っています。

1
2
3
4
5
6
7
8
9
assert.focus = function(aIterator, aFrame) {
  assert.equals(aFrame.location.href, aIterator.view.location.href);
  assert.equals(aFrame, aIterator.view);
  assert.equals(aFrame.document, aIterator.document);
  assert.equals(aFrame.document.body, aIterator.body);
  var docShell = getDocShellFromFrame(aFrame);
  assert.docShellEquals(docShell, aIterator.current);
  assert.isTrue(aIterator.isFindable);
}

複数の条件を満たしているかどうかを確認する必要がある場合、そのままテストを記述すると、同じコードがテストケースの中に大量に並んでしまいます。そういった一連の処理はカスタムアサーションとしてまとめておけば、テストケースの内容を綺麗に見やすくできます。

テストの実行

テストの実行は、MozRepl互換のUxUサーバを起動してコンソールから接続して行うか、MozUnit互換のテストランナーを起動して行います。「ツール」メニューから「UnitTest.XUL」→「MozUnitテストランナー」と辿り、テスト実行用のGUIを起動します。

UxUのテスト実行用GUIは、テストケースのファイルのドラッグ&ドロップを受け付けます。実行したいテストのファイル(docShellIterator.test.js)をウィンドウにドラッグ&ドロップすると「作業中のファイル」欄のファイルのパスがドロップされたファイルのものになりますので、後は「実行」ボタンを押すだけで自動テストを実行できます。

テストの現在の実行状況はプログレスメーターで表示されます。アサーションに失敗したり予期しないエラーが起こったりするとプログレスメーターが赤くなりますが、正常にテストが成功すれば緑色のままです。すべてのテストケースの実行結果が緑となることを目指して開発や修正を進めていきましょう。

まとめ

このように、一連の処理と期待される結果をまとめておき、自動的にテストできるようにしておくと、開発した機能がきちんと動作しているかどうかを人手を煩わさず機械的に確かめられます。一通りのテストケースを作成するにはそれなりの手間と時間を要しますが、一度作成しておけばテストは何度でも簡単に実行できますので、うっかりミスによるエンバグを未然に防ぐ*1ことができます。

Firefox用のアドオンはFirefox自身が持っている機能と連携して動作するように作られることが多く、自動テストを作るのはなかなか大変です。UxUには処理待ち機能をはじめとした多くの便利な機能があり、「このような操作を行った時に、こういう結果になる」といった人間の操作をシミュレートする形の複雑なテストでも、処理の流れを追いやすい形で記述できます。Firefox用アドオンの自動テスト作成にはまさにうってつけと言えるでしょう。

*1  エンバグしてしまってもすぐにそれに気がつくことができる

タグ: Mozilla | テスト | UxU
2008-06-11

GaUnit新API

Gauche 用の単体テストフレームワーク GaUnit の0.1.4がリリースされました。

Gaucheには標準でgauche.testという単体テスト用のモジュールが付 属しています。このモジュールはテストスクリプトをはじめから順 に実行していくという素直なテスト実行方式を採用しています。こ の方式では一連のテストがどのように実行されていくかがわかりや すい半面、(場合によっては)以下のような問題があります。

  • 特定のテストのみを実行することができない
  • 各テスト毎にgoshプロセスを起動する必要がある (各テストが他のテストの影響をうけないようにするため)
  • テストを別々に走らせるため、すべてのテスト結果を見るため には、一度ファイル(テストログ)に保存しておいてあとで確 認する、としなければいけない(テストを走らせる前にテスト ログを削除して前のテスト結果を削除する必要がある)

また、gauche.testはテストの成功、失敗にかかわらずテスト結果が 常に冗長であるという問題があります。テストが失敗した項目(修正 の必要がある項目)についてのみ情報を詳細する方が、より合理的で しょう。

ということで、GaUnitです。GaUnitは xUnit系の単体テ ストフレームワークで、上記のgauche.testの問題を解決します。 また、テスト失敗時以外はテスト結果に余計な情報を出力しないため 無駄がありません。そんなGaUnitが今回のリリース(正確には1つ前の 0.1.3)からより書きやすいAPIを提供して、以下の2点を行うだけで すむようになりました。

  • テスト用のモジュールを作成
  • その中でtest-始まりの手続きを定義

実際のコードは以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(define-module test-your-library
  (extend test.unit.test-case)
  (use your-library))
(select-module test-your-module)

(define (test-your-module-procedure1)
  (assert-equal "Good!" (your-module-procedure1))
  ...
  #f)

(define (test-your-module-procedure2)
  (assert-equal 29 (your-module-procedure2))
  ...
  #f)

(provide "test-your-module")

最後にtest-*手続きの最後に#fを付けているのは末尾再帰の最適化 でバックトレースを落とさないようにするためです。

新しいAPIでは、普通のGaucheライブラリと同じようにテストを書 けます。テストのために覚えることと言えば、どのassert-*を使お うかということくらいです。これは、普段の別のライブラリを用い た開発と同じですね。

GaUnitでのテスト書き方をもっと知りたい人はチュートリアル を読んでください。

タグ: テスト
2008-06-04

最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|
タグ:
RubyKaigi2008Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer SapporoRubyKaigi 2012 OfficialSponsor SapporoRubyKaigi 2012 Speaker