MongoDB で使用する JavaScript 関数をモジュール化して Nodeunit でユニットテストしよう、という話です。

2012 年 12 月現在の Wikipedia によると MongoDB は CommonJS の処理系ということになっているようですが、CommonJS Spec Wiki にその名は見当たりません。
CouchDB は入ってるんですが。

MongoDB を CommonJS 処理系として見たとき、まず一番に辛いのが require 関数が無いことです。
(そもそも require が CommonJS の仕様において必須なのかどうかとかはよく知らない)

require が無くて何が困るかというと、MapReduce に使う関数なんかをモジュール化して書けないので、ユニットテストを書くのが困難、ということです。

そこで、MongoDB において JavaScript コードのモジュール化を促すものを作ってみました。

yuya-takeyama/mongo_require.js

MapReduce に使用するモジュールを作成する

mongo_require.js を使えば、CommonJS と同様のやり方で JavaScript モジュールを作成し、それを MongoDB 内で使用することができます。
それでは定番の WordCount をやってみましょう。

CommonJS のやり方に従って、exports オブジェクトのプロパティとして mapper と reducer の関数をそれぞれセットしています。
ちなみに、ここでは使用していませんが module.exports も使用できます。

これをモジュールとして読み込んで、実際に MapReduce を実行してみましょう。

まず、mongo_require.js を適当な場所に保存します。

そして、MapReduce を実行するのは以下のスクリプト。

はじめに mongo_require.js を読み込むことで、mongo_require() 関数を有効にします。
mongo_require() 関数は CommonJS の require 関数とほぼ同様に使用できます。

ここでの mr オブジェクトにはプロパティとして mapper と reducer それぞれの関数を持っているので、それをそのまま mapReduce 関数に渡しています。

実際に実行すると、以下のように正しく単語数が集計できていることがわかります。
ここではあらかじめ texts コレクションに文が入っていたものとしています。

このように、関数をモジュール化した上で、それを利用しての MapReduce ができました。

モジュールをユニットテストする

関数をモジュール化できたのであれば、それをユニットテストするのも容易になります。

ユニットテストには Node.js など、別の CommonJS 処理系の、既存のテスティングフレームワークを使用することができます。

フレームワークは何でもいいのですが、とりあえずシンプルなもの、ということで今回は Nodeunit を使用してみます。
個人的に Node.js でモジュールを書くときは Jasmine で BDD スタイルに書くのが好きですが、今回のようなシンプルなモジュールであれば、Nodeunit ぐらい素朴なフレームワークの方が向いているのではないでしょうか。

ここでのモジュールの読み込みには普通に require() 関数を使用します。

これを実行すると以下のような結果が得られます。

このユニットテストのポイントとしては以下が上げられます。

  • Mapper 関数内で使用する emit() の大体となる関数をグローバル空間に定義している
  • Mapper 関数をテストするアサーション関数として mapperEmits() 関数を定義している

mapperEmits() 関数は第一引数として Mapper 関数、第二引数として処理するレコードを受け取り、そのレコードを処理したときに emit() されるレコードが第三引数と一致するかをチェックします。
(emit() と mapperEmits() については npm でパッケージ化することを考えています。)

ところで、グローバル関数に依存したテストを書くことは、本来であればアンチパターンとされています。
例えば大規模なプロジェクトにおいてはグローバル関数は極力避けることが望ましいのですが、ここではひとつの MapReduce をひとつの小規模なプロジェクトとして考えています。
プロジェクトの規模が小さければ、グローバル空間汚染によるコンテキストの複雑化もそれほどは問題にならないでしょう。

Reducer 関数については、通常は入力値を元に値を返すだけの参照等価な関数なので、特にこういった特別な関数を用意せずとも deepEquals() 関数でアサーションができます。

まとめ

mongo_require.js を使って MongoDB で使用する関数をモジュール化する方法と、そのモジュールを Nodeunit でユニットテストする方法について紹介しました。

MapReduce は元々 Mapper と Reducer というシンプルな二つの関数の組み合わせで大規模な計算を行う、というアイディアのもとに考えられています。
このシンプルな関数さえ正しく動作することが保証できれば、多少複雑な集計も安心して実装することができますね。ハピラキ。

, , , ,

jQuery で Ajax リクエストを行う場合, パラメータはハッシュ (JavaScript の Object 型) で指定して実行します.

この例だとまだシンプルに見えますが, success コールバックなどが肥大化してくると, 見通しが悪くなってきます.

そこで以下のようなコードを用意します.
jQuery.ajax() のラッパーとなる FluentAjax オブジェクトと, それを呼び出すヘルパーとして fajax() 関数を定義しました.

メソッドの定義にはメタプログラミングを使用しており, 実質上のプログラムは約 30 行程度です.

これを利用すると, 同じ Ajax リクエストを以下のように行うことができるようになります.

メソッドチェインでリクエストオプションを組み立てていき, 最後に execute() メソッドでリクエストを実行しています.
いわゆる Fluent Interface (流れるようなインターフェイス) になっていると思います.

メソッドチェインを使った方が可読性が高い… かどうかは好みの問題かもしれませんが, ひとつのサンプルとして.
これをもう少し抽象化すれば, 1 つのハッシュを引数にした複雑な関数は, 何でも Fluent Interface にできそうですね.

See also

, , ,

思いつきをメモ.

良い点

  • コンストラクタが隠蔽される
    JavaScript では, オブジェクトのコンストラクタは関数として定義されます.
    なので, new Constructor と呼び出すべきなのに, Constructor() というように呼び出されることもあり得ます.
  • インスタンスメソッドも隠蔽される
    Dog.create() でオブジェクトが生成されるまでは, greet() メソッドは見えません.
    オブジェクトを通してしか, インスタンスメソッドにアクセスできないということです.

悪い点

  • create() の定義が面倒
    create() に渡された引数を, init() に渡さないといけないので, 定義がやや煩雑です.
    Function.apply() が使えれば, arguments を渡すだけで良さそうですが, コンストラクタを Function.apply() で呼び出すことってできるんでしょうか?
    解決策としては, 引数を全てハッシュ変数 (Object) にする, というのが考えられます.
  • これで本当に幸になれるのか微妙
    JavaScript では private や protected といった修飾子で, メソッドを隠蔽することはできません.
    普通に定義する create メソと, コンストラクタもメソッドも明け透けな状態にあります.
    しかし, そこは開発者の良心に委ねることでも十分解決できるのではないでしょうか.
    「アンダースコアから始まるメソッド, プロパティはプライベート扱い」という紳士協定を共有すればいいのではないでしょうか.
    Python は, そういった文化の中で利用されている言語のひとつでしょう.