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

ククログ


Gitで不適切なコミットメッセージを削除した公開リポジトリを作る

分散バージョン管理システムのGitには様々なサブコマンドがありますが、その中の1つである git filter-branch を使用すると、過去のコミットを完全に無かった事にしてしまうなどの強力なコミット履歴の編集が可能となります。大きなリポジトリの特定のディレクトリ以下の内容をコミット履歴付きで別の小さなリポジトリとして取り出したりファイルの中に書かれていた生のパスワードを履歴の中から消去したり、というのはよく紹介される例です。

このエントリでは別の例として、コミットメッセージだけを後からまとめて修正する手順をご紹介しましょう。

元々非公開なプロジェクトとして開発を進めていたものを、公開リポジトリに移動したいとなると、やはり機密情報は完全に取り除いておく必要があります。リポジトリに格納されているファイルそのものの内容の編集方法については上記の例で解説されていますが、それ以外の場合として、コミットメッセージに書き込まれている情報を消去したいという事がたまにあります。

実際あったケースで問題になったのは、マージコミットのメッセージでした。

複数人で開発しているプロジェクトでは、自分のコミットをpushしようとするとエラーになってしまったので、一旦pullしてマージしてから再度pushする、という事がよく起こります。この時、gitが生成する既定のコミットメッセージには以下のように、pullしたリポジトリの位置が含まれてしまっています。

commit 213aa813611179b5cc4139e82f672922921e340a
Merge: a57bd32 511348d
Author: SHIMODA Hiroshi <shimoda@clear-code.com>
Date:   Wed Jun 15 15:44:15 2011 +0900

    Merge branch 'master' of github.com:piroor/treestyletab

この例ではgithubのリポジトリの位置が書き込まれていますが、プロジェクトの参加者全員で使用している中央リポジトリが秘密のサーバの上に置かれていた場合や、あるいは参加者の各PCの間で直接IPアドレスを指定するなどしてpullしあっていた場合には、pullしたリポジトリの位置として「192.168.1.2」のような公開される情報には相応しくない内容が出現してしまいます。

git pull --rebase としてpullすればこのような事は起こらないのですが、やってしまった物はもう仕方がありません。このようなコミット履歴がたくさんある時でも、git filter-branch を使用すると、コミットメッセージを任意の内容で書き換えた新しいリポジトリを作成する事ができます。具体的な手順は、以下の通りです。

% git clone git@internal.example.com:private-project.git
% cd private-project
% git filter-branch --msg-filter 'sed -e "s/Merge.*internal.*\$/Merge/"' -f

filter-branch サブコマンドの --msg-filter オプションには、任意のシェルコマンドを文字列として渡す事ができます。このコマンドに対しては、元のコミットメッセージが標準入力として流し込まれ、コマンドの実行結果の標準出力が新しいコミットメッセージとなります。上記の例のように sed などを使って文字列を置換すれば、あまり人目にさらしたくないコミットメッセージを削除するという事もできます(ここでは例として、「Merge」という文字列で始まり行中に「internal」という内容を含む行があれば、それをすべて単に「Merge」という文字列に置き換えています)*1

git log で編集後のコミットログを確認して、期待通りの編集結果が得られていれば、後は公開リポジトリにpushするだけです。

% git push git@public.example.com:public-project.git

以上、コミットメッセージの中に含まれた不適切な内容を書き換えた新しいリポジトリを作る手順をご紹介しました。皆さんもこの手順を使って、社内でしか共有されていなかった便利ツールなどを広く一般に公開してみてはいかがでしょうか。

*1 ただ、この時実際には、古いコミットメッセージを伴う元々のコミットと、新しいコミットメッセージを伴う全く別のコミットの両方が、リポジトリには含まれた状態になっています。元々のコミットはmasterなどのブランチに紐付けられていない迷子のコミットという扱いになるため、git log では通常は表示されませんが、git log --all とオプションを指定したり、リビジョンを直接指定した場合には、古いコミットメッセージが残っている事を確認できます。この時の作業用のリポジトリの内容を新しい空の公開リポジトリにpushする場合は、このような迷子のコミットはpushされないので気にする必要はありませんが、何らかの理由でその作業用のリポジトリを(ファイルコピーなどで)そのまま外部に公開するとなると、迷子のコミットであってもリポジトリの中に残っているのは危険です。このような場合には、git gc --prune=now でゴミを掃除して迷子のコミットを完全に消去し、それらを閲覧できないようにしてしまうとよいでしょう。

タグ: Git
2012-01-05

均衡待遇・正社員化推進奨励金: 正社員転換制度

クリアコードは、開発者と営業、人事、経理、総務・・・と幅広い業務を担当するもの1名で運営しています。 会社設立から5年以上が経過し、会社運営においても様々な経験をしてきましたので、今後は、技術ネタだけでなく会社運営に関する話題も発信していく方針です。 今回はクリアコードにおける助成金の活用について紹介します。

クリアコードではこれまでいくつかの助成制度を活用してきました。 現在は「均衡待遇・正社員化推進奨励金」という制度を活用しようと、就業規則の整備などを進めているところです。

均衡待遇・正社員化推進奨励金

この「均衡待遇・正社員化推進奨励金」はパートタイム労働者や有期契約労働者の雇用管理の改善を図るため、正社員への転換制度や正社員と共通の処遇制度などを設け、実際に制度を適用した事業主に対して支給する奨励金です。

奨励金には次の5種類があります

  1. 正社員転換制度の奨励金
  2. 共通処遇制度の奨励金
  3. 共通教育訓練制度の奨励金
  4. 短時間正社員制度の奨励金
  5. 健康診断制度の奨励金

このうち、正社員転換制度と健康診断制度を導入し、奨励金制度を活用しようと考えています。

正社員転換制度

まず、正社員転換制度の奨励金では、新たに転換制度を導入し、その雇用するパートタイム労働者を1人以上正社員に転換させると中小企業の場合40万円が支給されます。また2人目以降は1人あたり20万円が支給されます。

この奨励金を受けるにあたって壁となりそうなのが「正社員転換制度」の導入です。 正社員転換制度の導入では、まず正社員転換制度を作成し、それを労働協約または就業規則に明文化する必要があります。もちろん改定後の就業規則は労働基準監督署に届け出なければなりません。

クリアコードでは、東京労働局の雇用均等室(九段)でいただいたアドバイスをもとに、就業規則に正社員転換制度を定めました。参考までに正社員転換制度の条項を紹介します。

正社員転換制度
第X1条(総則)

この規定はパートタイマーの正社員転換制度について定めたものである。

第X2条(転換の条件)

正社員に転換することのできるパートタイマーは、本人が転換を希望し、かつ転換試験に合格した者とする。

第X3条(転換試験の受験資格)

正社員転換試験の受験資格は以下の各号のすべてを満たした者に与える。

(ア)勤続6ヶ月以上であること
(イ)フルタイム勤務できること
(ウ)心身ともに健康であり、職務に対する意欲があること
(エ)全国各地への転居を伴う異動を受け入れることができること
(オ)直属上司の推薦があること

第X4条(正社員転換試験)

正社員転換試験の内容は以下の通りとする。

(ア)業務知識に関する筆記試験またはプログラミング試験
(イ)人事担当者、直属上司、役員による面接試験

第X5条(申請の受付)

正社員への転換申請は毎年3月、6月、9月、12月の1日から5日に受け付ける。

正社員への転換を希望するパートタイマーは、この期間に転換の申請を人事担当者に行なう。

第X6条(審査、試験の実施)

1.正社員への転換申請があったとき、会社は第34条に定める要件を満たしているか否かを審査し、適格者に対して申請月の翌月末日までに転換試験を行なう。
2.試験の合否は、同月中に書面あるいは口頭により本人に通知する。

第X7条(辞令)

正社員への転換を認めたパートタイマーに対しては、試験合格が通知された翌月1日付で社員採用辞令を発令する。

第X8条(労働条件)

1.正社員に転換した者の労働時間・休日・休暇その他の労働条件は、正社員就業規則に定めるところによる。
2.年次有給休暇の勤続年数の算定においては、パートタイマー中の勤続年数を通算する。

以上が正社員転換制度についての条項です。

アルバイトから正社員になる方がいたり、今後アルバイトから正社員に登用したいと考えている会社では導入を検討されてはいかがでしょうか。

正社員転換制度を導入する理由

補足ですが、クリアコードが正社員転換制度を導入する理由についても紹介しておきます。

クリアコードの採用活動は人材紹介会社や公共職業安定所を活用しておらず、社員を通じてか、Webサイトの採用ページからの問合せのいずれかの方法で「プログラミングが好きであること」を満たした方にご応募いただいています。

この「プログラミングが好きであること」という採用条件は一定レベルのプログラミングスキルを求めているわけではなく、「プログラミングを仕事にしてやっていきたいという気持ち」を確かめるようなものです。なので、プログラマーとして仕事をした経験がなくてもOKです。

採用にあたってはプログラミングの能力やセンスはもちろん重要な選考基準となりますが、目の輝き(やる気)、コミュニケーション能力、社会人として仕事への考え方がしっかりしているかなど、プログラミング以外の能力にも注目します。

後者を重視した場合、プログラミングスキルが未熟であるがプログラマーとして採用するという現象が発生します。 この場合、採用時点では採用する側もされる側もプログラマーとしてやっていけるのか、またプログラマーとしてのスキルをどこまで伸ばすことができるのかわからない状態です。 そのため、一定のスキルを身につけるまではパートタイマーとして採用し、プログラミングスキルの習得にあてることにしています。

一般的には試用期間を設定した上で正社員として採用するケースが多いようですが、試用期間で切るというのは労使ともに厳しい決断になってしまうので、パートタイマーと正社員という大きな違いを設定しておいたほうが、会社としても採用しやすいという考え方もできるでしょう。

このような採用プロセス(パートタイマーから正社員へ)がちょうど正社員転換制度にあてはまることから、導入に至ったわけです。

次回予告

次回は、健康診断制度の導入について紹介します。

タグ: 会社
2012-01-11

デバッグしやすい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メソッドをオーバーライドする

タグ: Ruby | テスト
2012-01-18

均衡待遇・正社員化推進奨励金: 健康診断制度

正社員転換制度に引き続き、均衡待遇・正社員化推進奨励金の導入について紹介します。今回は「健康診断制度」の導入についてです。

健康診断制度

健康診断制度の奨励金は労働安全衛生法で健康診断が義務付けられている「常時雇用する労働者」に該当しないパートタイム労働者・有期契約労働者を対象とした健康診断制度を定め、実施するものに対して支給されるものです。制度を導入し、2年以内に延べ4人以上に健康診断を実施すると40万円が支給されます。

クリアコードでは、制度導入にあたりパートタイム用就業規則に、健康診断制度として以下の条項を追加しました。

健康診断制度
第X条(健康診断)

会社は次の要件を満たすパートタイマーに対して、健康診断を実施する。

(ア)常時雇用する労働者に該当し、安全衛生法で健康診断の実施が義務付けられているパートタイマーに対して雇入時健康診断および定期健康診断を実施する。
(イ)前項の常時雇用する労働者に該当しないパートタイマーのうち以下の条件のいずれかあるいはすべてを満たすパートタイマーに対して定期健康診断を実施する。

  • 週の所定労働時間が20時間以上30時間未満であるもの。
  • 雇用契約の更新により1年以上雇用されている、または雇用されることが見込まれるもの。

健康診断の費用は会社が負担する。

以上が健康診断制度の条項です。

ポイントは(イ)のところで、安衛法で健康診断が義務付けられていないパートタイマーのうち健康診断の対象となるものの条件を明記しています。

この条文についても、東京労働局の雇用均等室のアドバイスをもとに制定したものです。参考にされる際は、契約されている社会保険労務士の先生などにご確認ください。

まとめ

クリアコードではここ数年アルバイトの方にも活躍していただいています。健全なコードを書くためには健康な体が必要と考えており、アルバイトも含め全社員が健康診断を受けることにしました。

タグ: 会社
2012-01-24

モーショノロジー2012 #1: rroongaによる検索サービスの実装

注意: 長いです。

簡単まとめ: 検索サービスを作るにはrroongaが便利です。groongaサポートサービスをはじめます。

CROOZ株式会社が主催する「モーショノロジー2012 #1 全文検索&検索を利用したサービスの使命、利用プロダクト、事例紹介」が開催されました。今回のテーマは検索ということでgroonga開発チームに声をかけてもらいました。groonga関連の枠がいくつかあったのですが、ここではRubyとgroongaを使った検索サービスの作り方についての枠の内容を紹介します。

rroongaによる検索サービスの実装

以下、多少省略しながらスライドの内容を紹介します。

概要

話すこと

紹介する内容はrroongaを使った場合のメリット・デメリットと入力補完についてです。メリットは事例も交えながら紹介します。入力補完は「Ruby + groongaだからできる」という機能ではなくgroonga単体でも利用できる機能なのですが、最近の検索サービスでは当たり前になっている大事な機能なので、あわせて紹介します。

rroonga?

rroonga?

Rubyとgroongaを一緒に使う方法には以下の2つの方法があります。

  • groongaサーバーを起動し、HTTPで通信してgroongaの機能を使う方法
  • groongaをライブラリとして使用し、API経由でgroongaの機能を使う方法

全文検索システムはgroonga以外にもたくさんありますが、その中にSolrという全文検索システムがあります。Solrは全文検索エンジンとしてLuceneを利用したシステムです。groongaはLuceneと同じ全文検索エンジン機能もSolrと同じサーバー機能も備えています。groongaサーバーを起動する使い方はSolrのように使う使い方で、ライブラリとして使う使い方はLuceneのように使う使い方になります。

今回は後者のgroongaをライブラリとして使用する方法でのメリット・デメリットを事例を交えながら紹介します。

Rubyからgroongaをライブラリとして使うためにはrroongaというRubyのライブラリを使います。この方法ではアプリケーションがデータベースを持つことになります。

システム構成

システム構成

groongaをライブラリとして使う方法ではアプリケーションサーバーがデータベースを持つことになります。これは、よくあるアプリケーションサーバーとデータベースサーバーが分離している構成とは異なります。このあたりがメリット・デメリットにつながってきます。

メリット

メリット

groongaをライブラリとして使う場合、以下のようなメリットがあります。

  • 通信コストがないため、小さいコストで細かくデータを読み書きできる
  • 低レベルのAPIから高レベルのAPIまで使えるため柔軟に演算を組み合わせた検索ができる

小さいコストで細かいデータの読み書きができるメリットを活かした例は後で紹介します。まずは、柔軟に演算を組み合わせられるメリットを活かした例を紹介します。

組み合わせ例: 多段ドリルダウン

組み合わせの例の1つが「多段ドリルダウン」です*1。「ドリルダウン」は「ファセット」と呼ばれることの方が多いのですが*2、ECサイトなどでよく使われている機能です。

amazon.co.jpなどで絞り込める条件がリンクになっているインターフェイスを見たことがないでしょうか。あれがドリルダウン(ファセット)です。

ドリルダウン=ファセット

このスクリーンショットは「本」で検索した状態です。「本」カテゴリーのうち、さらに絞り込める項目(「コンピュータ・IT」、「ビジネス・経済」など)がリンクとしてリストされています。ここをクリックすると検索語などを入力せずに簡単に絞り込んでいくことができます。

また、各項目の横に「(5,248)」などヒット件数も表示されていることに気づいたでしょうか。これは「コンピュータ・IT」、「ビジネス・経済」などサブカテゴリーで絞り込んだ結果ヒットする件数を示しています。事前に検索システム側で検索して、0件ヒットする項目はそもそもこのリストに入らないようになっています。そのため、「絞り込んだけど0件ヒット」という無駄な検索を避けることができます。

このような点でより効率的に検索できるような機能なので、より広く使われるようになりました。

ドリルダウン

さて、多段ドリルダウンはどう違うかというと最終的な検索結果を求める途中でもドリルダウンをするという点が違います。

多段ドリルダウン

高レベルな検索機能しかない場合は、多段ドリルダウンを実現するために「全データ→途中結果」までの検索と「全データ→最終結果」までの検索を2回実施し、それぞれの検索結果に対してドリルダウンする必要があります。これは、高レベルな検索機能では検索の途中結果を保存しておいて再利用するような機能がないためです。

低レベルな検索機能も使えると「全データ→途中結果→ドリルダウン」をしてから「途中結果→最終結果→ドリルダウン」というように効率のよい処理を実現できます。

では、多段ドリルダウンが有用なケースはどのようなケースでしょうか。

用途

多段ドリルダウンは、絞り込み後に値を変更することが多い条件に有用です。amazon.co.jpのカテゴリーの例でいえば『「コンピュータ・IT」で絞り込んだ後に、やっぱり「ビジネス・経済」に変更しよう』ということが多いかどうかということになります。多段ドリルダウンを実施しておけば「絞り込み→解除→再絞り込み」という操作ではなく「絞り込み→再絞り込み」という操作を実現でき、少ない手順で検索できます。

例えば、「価格帯」が再絞り込みをしたくなるような条件です。最初は安めの価格帯で絞り込んでいたけど、よいのがなかったからもう少し高めのものも見てみよう、ということはよくありますよね。

デメリット

デメリット

デメリットはスケールアウトする標準的な仕組みがないことです。そのため、レプリケーションの仕組みを自分で作り込む必要があります。

開発事例

開発事例

Rubyとgroongaでテレビ番組を検索するWeb APIを開発しています。別のシステムから提供される番組情報をrroongaを使ってgroongaのデータベースへ取り込み、番組情報を検索するためのHTTP + JSONベースのWeb APIを提供するシステムです。このシステムが直接視聴者から利用されることはなく、番組検索APIを利用した連携アプリケーションがユーザー用のインターフェイスを提供します。

このシステムは外部からの更新がないとてもシンプルな構成のため、番組情報ソースを各アプリケーションサーバーにコピーし、各サーバーでそのソースを元にデータベースを構築することで冗長化・スケールアウトを実現しています。

このAPIを利用したアプリケーションの1つがテレコ!です。地上波・BS放送・CS放送の番組をメディア横断で検索できるテレビ番組情報サービスです。ぜひ利用してみてください。

メタデータの抽出

メタデータ抽出

番組検索Web APIではデータロードのときにもrroongaが活躍しています。その1つがメタデータの抽出処理です。

メタデータはドリルダウン条件として使えるため検索しやすいシステムを構築するためには重要な情報になります。番組情報の場合は出演者やカテゴリなどがメタデータとなります。メタデータは重要な情報なのですが、用意するにはそれなりの手間がかかるため、なかなか充実させることができません。そのため、ある程度機械的に抽出することで補うことが有効です。番組検索Web APIでは以下のようなコードで番組説明から出演者情報を抽出しています。

1
2
3
4
5
6
7
8
9
10
11
names = []
# 番組説明
description = "出演者: ビートたけし・所ジョージ"
# 人物テーブル
people = Groonga["People"]
# people.records -> ["ビートたけし", "明石家さんま"]
people.scan(description) do |record,|
  # 番組説明内に人物テーブル内の人物名があったら抽出
  names << record.key # "ビートたけし"
end
p names # -> ["ビートたけし"]

やっていることは「はてなダイアリーでのキーワード自動リンク」と同じことです。テキスト(番組説明)中から事前に用意した語(人物名)を抽出しています。なお、この処理のことを「multiple string matching」と呼ぶそうです。

これはgroongaの低レベルのAPIを使って実現します。ただ、この処理のときはデータベース内のデータを頻繁に参照することになるので、データロードするアプリケーションがデータベースを持っていないと時間がかかって実用的にはならないでしょう。

入力補完

最後に入力補完の実現方法について説明します。

入力補完

groonga本体にはサジェスト機能があり、この機能は以下の機能を提供します。

  • 補完: 一部分を入力するだけで完全な検索語を入力できるようにする機能(Googleの検索ボックスにもある機能)
  • 補正: 検索語の一部が間違っていても修正して正しい検索語にする機能(Googleでいえば「もしかして」機能)
  • 提案: 検索結果が多いときに絞り込み用の追加の検索語を提示する機能(Googleでいえば「他のキーワード」機能)
  • ユーザーの入力から統計的に学習し↑の3つを強化する機能

このうち補完機能を使った入力補完の実現方法について説明します。

補完例

groongaの補完機能は補完候補そのものの字面(例えば「万葉集」が補完候補なら「万」など)でなくても、ローマ字やひらがな・カタカナで入力しても補完候補を提示できます。これはIMEがOFFの状態でも利用できて日本語を利用した検索システムではとても便利です。Googleでも同様のことをできますが、amazon.co.jpではできないようです。

補完方法

補完方法には大きく分けて以下の2つの方法があります。

  • コンテンツベースの方法
  • 統計情報ベースの方法

コンテンツベースの方法ではすでにデータベース内にある既知の情報を補完候補とする方法です。番組検索Web APIの場合は番組名や人物名などが適切な補完候補になります。この方法ではデータベース内にある正しい補完候補のみを利用するので、間違った候補を出すことがないというメリットがあります。一方、候補数が少なくて思ったより補完してくれないということもありえます。例えば、「コナ」では「名探偵コナン」を補完してくれなくて「名探偵」と入力しないといけない、といったことがあります。

統計情報ベースの方法ではユーザーの検索履歴をアクセスログなどから収集・解析し、多くのユーザーが検索した語などを補完候補とします。この方法では「コナ」で「コナン」を補完候補とできる可能性があります。一方、ある程度の統計情報がないと適切な補完候補を抽出できなかったり、補完候補の精度が低くなってしまう可能性もあり、調整が必要になります。例えば、特定のキーワードをわざと多く検索してくるようなアクセスがあった場合はそのようなアクセスを無視するといったことが必要になるかもしれません。

まとめ

おさらい

rroongaを使った検索システムの構成とその構成ならではのメリットとデメリットを紹介しました。メリットはメタデータの抽出などgroongaの低レベルのAPIも利用しながら検索システムを構築できることです。デメリットは標準的なレプリケーションの仕組みがないため冗長化やスケールアウトの仕組みを自作する必要があることです。

また、rroongaを使った検索システムの構成とは関係ないのですが、入力補完の実現方法についても紹介しました。

お知らせ

お知らせ

groongaの開発元である有限会社未来検索ブラジルとMySQLからgroongaを使うためのソフトウェアmroongaの開発に参加している斯波さんとクリアコードでgroongaのサポートサービスを提供することにしました。サポート開始は2/29の予定ですがすでにお問い合わせは受け付けていますので、groongaサポートサービスに興味のある方はぜひお問い合わせフォームからご連絡ください。

*1 「多段ドリルダウン」は一般的な用語ではないので注意してください。名前がなかったので今回名前をつけただけです。

*2 Solrでもファセットと呼んでいます。

タグ: Ruby | Groonga
2012-01-26

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
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|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|