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

ククログ


Ruby/groonga 0.0.7

Ruby/groongaとActiveGroongaの新しいバージョンがリリースされました。

いつも通り、以下のコマンドでインストールできます。

% sudo gem install groonga

0.0.7は最新のgroonga0.1.4に対応しています。

groongaが正式リリース前なので、まだRuby/groongaユーザもあまり多くはありませんが、徐々に使われはじめています。例えば、えにしテックスープカレー好きdaraさんが作ったbuzztterでRuby/groongaが使われています。

daraさんからはRuby/groongaに対するパッチをいくつかもらったりもしたので、Ruby/groongaのコミッタになってもらいました。APIの相談にものってくれる頼もしいCTOです。

今回のリリースでも便利な機能が入っているので、いくつか紹介します。

キーワードリンク

groongaを使ってはてなのようなキーワードリンクをRubyで付与することができます。

グニャラくんのところではSennaを使っていますが、同様の機能をRuby/groongaにも取り込みました。

Ruby/groongaを使うと以下のように書けます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- coding: utf-8 -*-
require 'groonga'

Groonga::Context.default_options = {:encoding => "utf-8"}
Groonga::Database.create
words = Groonga::PatriciaTrie.create(:key_type => "ShortText",
                                     :key_normalize => true)
words.add('リンク')
words.add('リンクの冒険')
words.add('冒険')
words.add('')
words.add('ガッ')
words.add('MUTEKI')
text = 'muTEkiなリンクの冒険はミリバールでガッ'
tagged_text = words.tag_keys(text) do |record, word|
  "[#{word}(#{record.key})]"
end
puts tagged_text
  # => [muTEki(muteki)]な[リンクの冒険(リンクの冒険)]は
  #    [ミリバール(ミリバール)]で[ガッ(ガッ)]

クエリからスニペット生成

groongaでは独自の構文のクエリから検索条件を指定することができます。buzztterで検索条件を指定するところでも使われています。例えば、以下のような構文があります。

  • 「単語1 単語2」: 単語1と単語2の両方にマッチする条件
  • 「単語1 OR 単語2」: 単語1または単語2にマッチする条件
  • 「単語1 - 単語2」: 単語1にマッチするが単語2にマッチしない条件

もう少し詳しい説明はgroongaのチュートリアルに載っています。

ここまでは前のリリースでもできたところです。今回のリリースからはさらにスニペット(検索語周辺のテキスト)も簡単に生成できるようになりました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# "description"カラムに「ruby」または「groonga」が入っているレコードを検索
query = "ruby OR groonga"
records = table.select do |record|
  record["description"].match(query)
end

# 「ruby」または「groonga」が含まれる周辺のテキストを表示
tags = [["<", ">"]]
records.each do |record|
  puts record["name"]
  snippet = records.expression.snippet(tags, :normalize => true)
  snippet.execute(record["description"]).each do |text|
    puts "==="
    puts record["description"] # => Rubyでgroonga使って全文検索
    puts "---"
    puts text                  # => <Ruby>で<groonga>使って全文検索
  end
end

どのようになるかはRuby/groongaのサンプルアプリケーションで試してみてください。ここの検索ボックスもクエリ文字列に対応しているので、「OR」や「-」を使ったクエリを使うことができます。

まとめ

Ruby/groonga 0.0.7の新機能を紹介しました。

groongaの機能を手軽に使えるRuby/groongaを試してみてはいかがでしょうか。

タグ: Ruby
2009-10-02

milterでtaRgrey

milter-greylistでtaRgreyでmilter-greylistにtarpitの機能が取り込まれたことを紹介しました。当時はまだ取り込まれただけでリリースはされていなかったのですが、先日、開発版としてmilter-greylist 4.3.4がリリースされました。

興味のある方が簡単に試せるようにDebian GNU/Linux lenny i386用とUbuntu 8.04 LTS Hardy Heron i386用のパッケージを作成しました。

それでは、milterだけでtaRgreyを実現する方法を説明します。milter-greylist単体で実現すると設定が煩雑になるので、milter managerとmilter-greylistを連携させます。

設定

milter managerのUbuntu用インストールドキュメント通りにインストールされているものとします。Debian用のインストールドキュメントはまだHTMLで公開していないので、下書きを参考にしてください*1

ただし、milter managerは1.3.1(開発版)以降を利用してください。1.3.1のパッケージは以下にあります。

Ubuntu 8.04 LTS Hardy Heron用apt-line:

deb http://milter-manager.sourceforge.net/ubuntu/ hardy universe
deb-src http://milter-manager.sourceforge.net/ubuntu/ hardy universe

Debian GNU/Linux lenny用apt-line:

deb http://milter-manager.sourceforge.net/debian/ lenny main
deb-src http://milter-manager.sourceforge.net/debian/ lenny main

設定は以下のように変更します。

  • Postfix: milterとのやりとりのタイムアウト時間を150秒にする
  • milter-greylist: 125秒のtarpitに耐えた接続をホワイトリストに入れる

milter managerの設定を変更することがない点に注意してください。milter managerは環境にあわせて適切なデフォルト値を使うので、システム管理の手間を減少することができます。

Postfixは/etc/postfix/main.cfに以下の行を追加してください。

/etc/postfix/main.cf:

milter_command_timeout = 150

設定を反映するために再起動します。

% sudo /etc/init.d/postfix restart

milter-greylistは/etc/milter-greylist/greylist.confを以下のように変更します。

変更前:

racl greylist default

変更後:

racl whitelist tarpit 125s
racl greylist default

125秒のtarpitに耐えたらホワイトリスト、耐えられなかったらgreylistで救済という動作が自然に書けています。

設定を反映するために再起動します。

% sudo /etc/init.d/milter-greylist restart

最後に、milter-managerに設定の変更を検出させるために再起動します。設定の変更は必要ありません。

% sudo /etc/init.d/milter-manager restart

milter-managerの設定内容を確認すると、milter-greylistのタイムアウト時間が135秒となっているのがわかります。

% sudo /usr/sbin/milter-manager -u milter-manager --show-config
...
define_milter("milter-greylist") do |milter|
  ...
  milter.writing_timeout = 135.0
  milter.reading_timeout = 135.0
  ...
end
...

135秒は「デフォルトのタイムアウト時間10秒」と「milter-greylistのtarpit時間125秒」を足し合わせた時間です。これはmilter-managerが自動で検出します。

これで、milterによるtaRgrey環境が完成です。

まとめ

tarpit対応のmilter-greylistがリリース(ただし開発版)されたので、milter managerとmilter-greylistを組み合わせてtaRgreyを実現する方法を紹介しました。

milter managerもmilter-greylistもどちらも開発版を使用しているので、まだ本番環境への投入は早いですが、Debianパッケージがあるので比較的簡単に試せるようになっています。検証など興味のある方は試してみてはいかがでしょうか。問題があった場合は、それを報告し、安定版リリース時には問題が解決されているようにしたいですね。

*1 次のリリースで公開する予定

2009-10-06

入門GTK+

注: 脚注がたくさんあります。

そろそろオーム社から入門GTK+が出版されます。最後に日本語のGTK+関連書籍が出版されたのが2002年なので、実に7年ぶりです*1

クリアコードにはGTK+に精通している開発者が在籍していたり*2、GTK+を利用したソフトウェアに関するサポートを提供していたり*3と日本でGTK+がもっと普及して業務でよりクリアコードの技術力を活かせる機会が増えることを期待しています。

というように期待している入門GTK+ですが、レビュー他で少し参加したため一足早く手元に届いています。興味がある方が購入するかどうかを検討するための材料になるようにGTK+まわりの現状も含めながら紹介します。

概要

入門GTK+ではざっくりまとめると以下を解説しています。

  • GTK+でよく使われるウィジェットの使い方
  • ドラッグアンドドロップなどGUIアプリケーションでよく使われる機能の作り方をいくつか
  • GLib*4の便利なデータ型や関数の使い方をいくつか
  • GdkPixbuf*5の使い方とGTK+との連携の仕方
  • cairo*6の使い方

一方、以下は含まれていません。

  • GObject*7の使い方
  • GIO*8の使い方
  • GDK*9について
  • Pango*10の使い方
  • GNOME*11と連携したアプリケーションの作り方
  • Poppler*12、GStreamer*13、librsvg*14などGNOMEで利用されているさまざまなコンテンツを扱うライブラリの使い方
  • GObject Introspection*15、GJS*16/Seed*17など、最近のGNOMEまわりで興味深いライブラリの使い方

もうすでにGTK+を使ってアプリケーションを書いている人には物足りないかもしれません。ただ、名前の通り、入門書なので上記のように線を引いたことによりとっつきやすくなっている印象があります。各種Linux(Ubuntu、Fedora、OpenSUSE)やWindows(Visual Studio 2008 Express Editionを使用)での開発環境の作り方や(簡単な)Gladeなど周辺開発支援ソフトウェアの使い方が載っているのも入門者には嬉しいのではないでしょうか。

GTK+は最近のバージョンに対応しています。昔、GTK+アプリケーションを書いていたという方には多少得るところがあるもしれませんが、最近のGTK+事情などは書かれていないので、消化不良になるかもしれません。本書の売れ行きが続編や加筆に影響するかもしれないようなので、応援の気持ちで購入するというのはアリかもしれません。

最近のGTK+/GNOMEをキャッチアップできている方には向いていないでしょう。

流れ

本書の流れを簡単に紹介します。

まず、簡単なGTK+アプリケーションをチュートリアル形式で作ります。読み進めていくにしたがって徐々にアプリケーションができあがっていくのでGTK+アプリケーションの雰囲気をつかむのによいのではないでしょうか。ここがあることで、GTK+を触ったことがない方がスタートでつまづいて諦める、というシチュエーションが減るのではないでしょうか。

続いて、GTK+のウィジェットの配置とシグナルについて説明します。配置のところはスクリーンショットが多く、説明が直感的に理解しやすいでしょう。

その後、GLib、GdkPixbuf、cairoとGTK+の周辺ライブラリの解説になります。

GLibの部分はそれほど厚く解説されているわけではありません。例えば、GStringまではカバーしていません。GStringはGLibを使いこんでいる人には便利さが身に染みてわかっているオブジェクトの一つですが、GListやGHashTableの方が優先されています。GLib部分を期待しすぎると物足りなく感じるかもしれません。

GdkPixbufは一通り解説されているので、簡単に使う分には困らないでしょう。アニメーションや拡大・縮小・回転については触れられていません。

cairoは基本的な考え方からしっかり解説されています。スクリーンショットが豊富なのでわかりやすいです。Rubyist Magazine - cairo: 2 次元画像描画ライブラリでは省略されている部分も解説されています。日本語でcairoの基礎的な部分を解説している文章では一番まとまっていてわかりやすいものかもしれません。隠れたオススメポイントと言えるでしょう。

周辺ライブラリに触れた後、またGTK+に戻ります。GTK+でよく使われるウィジェットの使い方をスクリーンショット付きで解説します。スクリーンショット付きなので、この部分をパラパラめくりながら使えそうなウィジェットを探すという使い方ができて便利そうです。ここもオススメポイントです。

最後に、簡単な独自ウィジェットの作り方などもう一歩先へといった感じの内容があります。

というように、GTK+の知識がそれほどない方でも読み進めていきながら徐々に理解を深められる流れになっています。

まとめ

久しぶりにGTK+の日本語書籍が出版されるので、GTK+まわりの現状も含めながら内容を紹介しました。日本でGTK+がより広く使われるようになることを期待しています。

また、クリアコードにもGTK+関連のお話がくることも期待しています。会社ブログっぽいですね。

入門GTK+
菅谷保之
オーム社
¥ 3,240

*1 一方、Qt関連の日本語書籍は定期的に出版されています。日本ではQtの方が多く使われているのでしょうか。フリーソフトウェア・オープンソースソフトウェアとして公開されているものだけみると大差はないようにみえます。

*2 GTK+の日本語入力まわりや、GeckoやWebKitなど複数のレンダリングエンジンを切り替えられるGTK+を用いたWebブラウザや、RubyからGTK+を利用するためのバインディングやプレゼンツールなどの開発に深く関わっていたりなど。GNOMEの開発チームにも参加している。

*3 Mozillaサポートを提供しています。Linux版のMozilla Firefox/ThunderbirdはGTK+を使っています。他に、GLib/GTK+を利用しているCutterのサポートや、Linux上で動作するクライアントソフトウェアのサポート・開発も行っています。

*4 ポータブルなユーティリティライブラリ

*5 ラスタ画像操作ライブラリ

*6 ベクタ描画ライブラリ

*7 C言語でオブジェクト指向を行うためのライブラリ

*8 ファイルシステムやソケットなど入出力に関する機能を提供するポータブルなライブラリ

*9 X Windows SystemやWindows API、Quartzなど環境依存のGUIのしくみを抽象化するライブラリ

*10 多言語に対応したテキストレイアウト・描画ライブラリ

*11 GTK+を用いたデスクトップ環境

*12 PDFパーサ・描画ライブラリ

*13 音楽や動画などマルチメディアコンテンツを扱うためのフレームワーク

*14 SVG描画ライブラリ

*15 GTK+のようにC言語で書かれたライブラリとRubyやPythonやJavaScriptなど他の言語の連携を支援するライブラリ

*16 SpiderMonkeyをC言語から使うためのライブラリあるいはGTK+などのC言語ライブラリをSpiderMonkeyから使うためのライブラリ。

*17 JSCoreをC言語から使うためのライブラリあるいはGTK+などのC言語ライブラリをJSCoreから使うためのライブラリ。Seedの方がよさそう。

この記事の続き

2009-10-14

milter manager 1.4.0リリース

milter manager 1.3.1にほとんど問題がなかったので、ドキュメントやOpenDKIMへの対応などを加えたmilter manager 1.4.0をリリースしました。1.4.0でログまわりを強化する予定もあったのですが、現時点で安定しているので、1.4.0での採用は見送り、次回のリリースにまわすことにしました。

今のところ、milter managerは新しい安定版をおよそ3ヶ月毎の間隔でリリースしています。そのたびに、新しい機能や最新のmilterへの対応が強化されています。また、バグも修正されているので、古いバージョンを使っている方はアップグレードをおすすめします。インストール・アップグレードの方法は以下のドキュメントを参考にしてください。

クリアコードではmilter managerの導入支援やmilterを用いたメールフィルタの開発などサーバサイドの迷惑メール対策サービスからMozilla Thunderbirdを用いたクライアントサイドの迷惑メール対策まで、メールシステム全体での迷惑メール対策サービスを提供しています。迷惑メールに困っている方はinfo@clear-code.comまでお問い合わせください。

2009-10-15

CPUの使用率を表示するFirefoxアドオンをリリース

CPUの使用率をFirefoxのウインドウ右上に表示するアドオンをリリースしました。ダウンロードはこちらからどうぞ。新しいバージョンがすでに公開されていますので、そちらを参照して下さい。

インストールすると、以下のイメージのようにウインドウ右上に直近のCPU使用率がグラフで表示されます。

CPUモニター

プラットフォームは、WindowsXP SP1以降、Mac OS X、Linuxをサポートしています。Linuxではlibgtop2を使用していますので、別途インストールしてください。

また、このアドオンではCPU使用率をWebアプリケーションからも使えるようにしてあります。JavaScriptで以下のように書くと、intervalTime間隔ごとにfunctionが呼び出され、aUsageにその時のCPU使用率が渡ってきます。

system.addMonitor("cpu-usage", function(aUsage){}, intervalTime);

クリアコードでは、このようにWebアプリケーションから各種ハードウェアデバイスにアクセスする機能の開発を行っております。ご用命はinfo@clear-code.comまでお問い合わせください

タグ: Mozilla
2009-10-19

CPUの使用率を表示するFirefoxアドオン「システムモニター」を更新しました

CPUの使用率をFirefoxのツールバー上に表示するアドオン「システムモニター」のバージョン0.2をリリースしました。以下のリンク先からダウンロードできます。

前のバージョンからの変更点は以下の通りです。

  • グラフの左右端をドラッグすることで任意の幅に変更できるようになりました。
  • ツールバーのカスタマイズでグラフを任意の位置に移動できるようになりました。(初回起動時に、ツールバーに項目を追加するかどうかの確認を求めるようになりました。)

導入手順

インストールすると、初回起動時に以下のイメージのようなダイアログが表示されます。

初回起動時の確認ダイアログ

「はい」を選択すると、メニューバーの右端(Mac OS Xではナビゲーションツールバーの右端)に、以下のイメージのようにCPU使用率のグラフが表示されます。

CPUモニター

プラットフォームは、WindowsXP SP1以降、Mac OS X、Linuxをサポートしています。Linuxではlibgtop2を使用していますので、別途インストールしてください。

Web APIの利用

このアドオンを導入した環境では、システムモニターが提供するAPIを通じて、Webページ上のスクリプトからシステムモニターと同じ情報を取得できるようになります。以下は、Webページ内にCPU使用率のグラフを埋め込む例です。実際に上記リンク先からアドオンをインストールした状態で、このページを表示してみて下さい。

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
var container = document.getElementById("system-monitor-demo");
if (!window.system || !window.system.addMonitor) {
  container.innerHTML = "システムモニターがインストールされていません";
  container.style.color = "red";
} else {
  container.innerHTML = "<div><canvas id='system-monitor' width='300' height='60'></canvas>"+
                        "<br /><span id='system-monitor-console'></span></div>";

  var width = 300;
  var interval = 1000;

  var CPUTimeArray = [];
  var arrayLength = width / 2;
  while (CPUTimeArray.length < arrayLength) {
    CPUTimeArray.push(undefined);
  }

  // リスナとして登録する関数には、CPUの使用率が割合として渡される。
  function onMonitor(aUsage) {
    var console = document.getElementById("system-monitor-console");
    console.textContent = aUsage;

    CPUTimeArray.shift();
    CPUTimeArray.push(aUsage);

    var canvasElement = document.getElementById("system-monitor");
    var context = canvasElement.getContext("2d")
    var y = canvasElement.height;
    var x = 0;

    context.fillStyle = "black";
    context.fillRect(0, 0, canvasElement.width, canvasElement.height);

    context.save();
    CPUTimeArray.forEach(function(aUsage) {
      var y_from = canvasElement.height;
      var y_to = y_from;
      if (aUsage == undefined) {
        drawLine(context, "black", x, y_from, 0);
      } else {
        y_to = y_to - (y * aUsage);
        y_from = drawLine(context, "green", x, y_from, y_to);
        drawLine(context, "black", x, y_from, y_to);
      }
      x = x + 2;
    }, this);
    context.restore();
  }

  function drawLine(aContext, aColor, aX, aBeginY, aEndY) {
    aContext.beginPath();
    aContext.strokeStyle = aColor;
    aContext.lineWidth = 1.0;
    aContext.lineCap = "square";
    aContext.moveTo(aX, aBeginY);
    aContext.lineTo(aX, aEndY);
    aContext.closePath();
    aContext.stroke();
    return aEndY - 1;
  }

  // リスナを登録する。
  window.system.addMonitor("cpu-usage", onMonitor, interval);

  window.addEventListener("unload", function() {
    window.removeEventListener("unload", arguments.callee, false);
    // ページのアンロード時にリスナの登録を解除する必要がある。
    window.system.removeMonitor("cpu-usage", onMonitor);
  }, false);
}
["
\n", "\n"]

次世代のWebアプリケーションに向けて

上の例をご覧いただいても分かるとおり、このアドオンは、Webアプリケーション(Webページ上のスクリプト)から各種ハードウェアデバイスへアクセスする技術のデモンストレーションとして開発されています。

Webカメラとの連動によるAR(拡張現実)や、指紋認証、Felica認証といった特殊なハードウェアを必要とする認証技術など、この技術を応用することにより今までのWebアプリケーションではできなかった様々なことが可能となります。

クリアコードでは、このようにWebとハードウェアを直結する技術の開発に取り組んでおります。特殊なデバイスを搭載したハードウェアへのWebブラウザエンジンの組み込みや、特殊なデバイスの利用を前提としたWebベースのシステム開発など、ご用命がございましたらぜひinfo@clear-code.comまでお問い合わせください

タグ: Mozilla
2009-10-20

とちぎRuby会議02の資料公開

先日開催されたとちぎRuby会議02で社長の一人として話しました。声をかけてくれたtoRubyのみなさん、ありがとうございます。

儲かるRuby - 支えるRuby

システム構成

Debian GNU/Linux sidが動いているMacBookを持っていきました。事前の接続テストでうまくプロジェクターに出力できなかったので、ワイクル株式会社kdmsnrさんのMacBook経由で出力しました。突然のお願いにもかかわらず快く貸してくれました。ありがとうございます。

システム構成

Rabbit本体はDebian上で動いていて、携帯電話からのリモート操作もDebian上のRabbitに対して行っています。しかし、画面表示は別マシンのMac OS X上のX11で行っています。プロジェクターへの出力も別マシンのMac OS Xが行っています。

dRubyなど咳プロダクツを用いたプレゼン環境の1つのパターンのデモとして、上記のような構成を用いました。かっこいいですね。

内容

8割ほどRubyととちぎをまじえた発表者紹介・会社紹介をし、最後にかるく現在の仕事の内容、これから向かおうとしている方向を紹介しました。他の方たちのように具体的な方法は提示していません。私たちも続けられるしくみを模索しているのが現状です。

雰囲気がよい

とちぎRuby会議02はとても楽しく居心地のよい雰囲気に満ちていました。どうしてなのかはわかりません。まだ体験していない人はぜひ一度体験してみることをおすすめします。toRubyに参加すると体験できるでしょう。

まとめ

地域Ruby会議2ndシーズン最初のとちぎRuby会議02に参加しました。

お昼ご飯をゆっくり食べていたおかげで開始時刻が遅れてしまい、すみませんでした。あのよい雰囲気の中でtoRuby勉強会を味わう時間が減ってしまったことが残念です*1

とちぎRuby会議にはRuby札幌の方も参加していましたが、12月には札幌Ruby会議02があります。こちらにも発表者として参加する予定です。とちぎRuby会議02のようにとてもすばらしい地域Ruby会議になりそうな予感がします。都合のあう方は参加してみてはいかがでしょうか。

札幌Ruby会議02までには関西Ruby会議02TokyuRuby会議01もあります。こちらの内容もとてもおもしろそうですね。

*1 お昼ご飯タイムもとてもよい雰囲気でした。

タグ: Ruby
2009-10-25

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

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
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|