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

, , , ,

Espérance という BDD っぽくアサーションを行うライブラリを書いています。
とりあえず PHPUnit の中で使ってみるイメージはこんな感じです。

あと、前に中途半端に作った xSpec フレームワーク Speciphy と組み合わせるとこんなイメージにもできそうです。 (まだ作ってませんが)

なんで should でなく expect なのか

RSpec 3 からは expect がデフォルトになる、という記事が先日ありました。

RSpec’s New Expectation Syntax

RSpec といえば “foo”.should == “foo” といった具合に、全てのオブジェクトに should メソッドを生やしての アサーション (エクスペクステーション) が特徴でしたが、やっぱり Kernel モジュールに勝手にオブジェクト足されるのは気持ち悪いよね、ということで古き良き Test::Unit を使い続ける人も一定数いるように思われます。

should と比較して、expect は RSpec コンテキスト (多分 ExampleGroup とか Example とかの中だろうけど特に読んではいません) の中に存在するメソッドなのでグローバル空間に副作用がありませんし、それでいて英語としても読み下しやすい DSL だと言えます。

そして expect といえば、JavaScript の BDD ライブラリでは結構ベタだったりします。
Jasmine もそうですし、Socket.IO の作者による expect.js というものもあります。
Espérance は expect.js を PHP に移植して作っています。

PHPSpec について

PHP はオープンクラスでないので、全てのオブジェクトに should メソッドを生やす、なんてことはできません。

そこで PHPSpec では $this->spec(“foo”) とすることで、値を Interceptor オブジェクトでラップし、Interceptor がマジカルな DSL を実現する、という作りになっています。

PHPSpec の Interceptor は独立したライブラリではありませんが、Speciphy でも拝借しており、独立性の高いモジュールでもあります。

PHPSpec の Interceptor は凄いんですが、正直言って頑張り過ぎなんではないかと思っています。

PHPSpec 本体の spec を見ていると $this->matcher->getDescription()->should->be(‘be equal to 1′); なんてことをしていたりします。

$this->expect($this->matcher->getDescription())->to->be(‘be equal to 1′); とかで充分やん、とか思ってしまうわけです。

とはいえ Espérance も __get とか __call とか使ってますし、マジカルであることに変わりはないのですが、作りは大分シンプルになっていると思います。
expect.js の実装を真似ているので、expect.js が凄いというだけの話でもあるのですが。

expect.js の DSL の実装について

ところで expect.js では expect(1).to.be(1) といったプロパティのチェーンを、Assertion オブジェクトを再帰的に生成して to プロパティに新しい Assertion を差し込む、なんてことをしており、一度 expect(1) としただけで 40 数回ぐらいコンストラクタが呼ばれるています。

PHP では __call でプロパティ読み出しをオーバーライドすることができるので、Espérance では再帰的なコンストラクタは行われません。

JavaScript でも最近の処理形だと Object.defineProperty() とかで似たようなことができるような気がするのですが、それをしないのはより幅広い環境での使えるようにということなんでしょうか。 (JavaScript あんまり知らないのでよくわかりません)

名前について

Google 翻訳に expectation と入力してフランス語として出てきた単語から適当に選びました。

フランス語を名前として使うアイディアは、最近話題になった React (通称 node.php) でも使われているイベントディスパッチャライブラリ Événement のパクリです。

今後について

Speciphy もそうですが、ボクはライブラリやフレームワークを作ってもメンテが続かないタイプの人間だということだけ書いておきます。

でもまぁこういう感じでアサーションだけがライブラリと独立しているのは割とありなんじゃないかなーとは思いますし、Packagist への登録ぐらいはやっておきたいと思っています。思っているだけですが。

というわけでレバ刺しを食べに行きます。

, ,

Symfony2 はいわゆるフルスタックな Web アプリケーションフレームワークではありますが, 各コンポーネントは疎結合になっており, それぞれは Symfony Component として再利用できる形で公開されています.
その中でも Console コンポーネントBehatComposer, また Stagehand_TestRunner など, あらゆる PHP アプリケーションに利用されています.

その Console コンポーネントを使った際に, 出力のユニットテストを行うための SpyOutput というものを書きました.
これは [エラー: isbn:0131495054:n というアイテムは見つかりませんでした] でいうところの Test Spy という方法を使ってテストを行います.

yuya-takeyama / console-output-spyoutput

Test Spy とは

分類についてちゃんと理解できていない可能性もあるので, 今の所の自分の理解として書きます.

Test Spy オブジェクトはモックやスタブ同様, テストのために使用する偽物のオブジェクトの一種です.
本来使うべきオブジェクトの代わりにテスト対象に差し込んで, テスト対象の動作を行ったあとで, Test Spy に問い合わせることで検証を行う, というものです.

説明だけではわかりにくいと思うので以下の PHPUnit によるテストコードをご覧ください.

Application クラスはコンストラクタで OutputInterface (Console コンポーネントに含まれるインターフェイスです) を実装したオブジェクトを受け取り, 出力はそのオブジェクトに委譲します.

本来であれば ConsoleOutput 等をセットするところを, 代わりにこの SpyOutput をセットしています.
SpyOutput は OutputInterface を実装しているので, タイプヒンティングで OutputInterface が指定されていても問題ありません.

ConsoleOutput は write() や writeln() といったメソッドに文字列を渡すことで, その文字列を標準出力に出力しますが, SpyOutput は代わりにインスタンス変数内に溜め込みます.

Application オブジェクトの実行が完了したあとに, getMessage() メソッドで SpyOutput オブジェクトに問い合わせることで, 本来であればどのような出力がされるところだったのか, を問い合わせることができます.

車輪の再発明では無いのか

作ってから気づいたのですが, Console コンポーネントには既に出力をテストするためのクラスが存在していました.

Console コンポーネントはそもそもの謳い文句が “Console eases the creation of beautiful and testable command line interfaces.” となっており, いわばこれらのクラスもひとつのウリである, と考えられます.

ちょっと考えましたが, それでも SpyOutput は私にとって必要だと思いました.

これらは Console コンポーネント標準の Application や Command といった, 粒度の荒い単位でのテストを想定して作られたものであり, それより粒度の細かい単位のコンポーネントがある場合は適用することができません.

そもそも私がこれを必要としたのは, あるコマンドラインアプリケーションにおいて, その進捗状況を表示する Progress というコンポーネントを書いていて, そのテストに必要だった, というのがあります.

Progress の使用イメージは大体以下のような感じです.

これを Command という単位でテストしようとした場合, Progress とは関係の無い出力までそこに含まれてしまう可能性があります.

その可能性をできる限り排除するには, Application でも Command でもなく Progress 自体をユニットテストするのが良いでしょう.

このように, SpyOutput を使うことで, Progress だけをテストすることができました.

Progress は Symfony の Console コンポーネントの Application や Command のことは知らず, OutputInterface のみにしか依存していません. 進捗表示機能を SomeCommand 自体に持たせるのに比べると, 疎結合で, 再利用性の高い設計と言えるでしょう.

命名問題

長々と SpyOutput について説明してきましたが, 実はこのライブラリまだきちんとしたリリースができていません.
というのも, ベンダープリフィクスをどうすべきか迷っていて, 今のところ Perl っぽく X をつけて SymfonyX などとしているためです.

Symfony のためのライブラリとはいえ, 勝手に Symfony というベンダープリフィクスを使うのは多分ダメだろうし, そうなると何というプリフィクスを付けるべきでしょう?
どなたか教えてください.

(Symfony 本体に Pull Request 投げて取り入れてもらえると楽なんだけど…)

まとめ

この記事では Symfony の Console コンポーネントの出力をテストするライブラリ SpyOutput と, Test Spy というテスト手法について説明しました.
命名問題が解決したら, Packagist 辺りに公開するつもりです.

, ,