FizzBuzz問題をstreamで解く

動機的なもの

別に Stream である必要はないんだけど、Nodejs でデータを扱うなら Stream を通したいですよね! pipe 使いたいですよね!!

(countStream).pipe(fizzBuzzStream).pipe(process.stdout);

コード的には上記のように書きたいので、とりあえず機能ごとに各ストリームに分離する。

  • 1, 2, 3 ... リミットの数 とカウントして その数を流すストリーム
  • 数字を 'Fizz', 'Buzz', 'FizzBuzz' に変換するストリーム
  • 文字列をコンソールに書きだすストリーム
さて、

FizzBuzzStream の実装を自前でやると面倒くさいです。なので、npmライブラリの through を使うと吉です。
through 自体は文字通り throughストリームを作るライブラリ。

fizzbuzz-stream.js

var require('through');
module.exports = function () {
    return through(
        function _write (data) {
            var num = Number(data);
            this.emit('data', num % 15 === 0 ? 'FizzBuzz' :
                              num %  3 === 0 ? 'Fizz' :
                              num %  5 === 0 ? 'Buzz' : data
                     );
        }
      , function _end () { this.emit('end') }
    );
};

countStreamを作るモジュールは記事の最後に記載しますが、ここでは準備されているものとして、 FizzBuzz問題を解いてみます。

fizzbuzz.js

var createCountStream    = require( __dirname + '/count-stream');
var createFizzBuzzStream = require( __dirname + '/fizzbuzz-stream');

// process.stdout.write は改行コードを吐かないので細工する
// fizzbuzzStream の方で改行コードをくっつけて流せばいいんだけど、
// なんか気味悪い感じするので...
process.stdout.write = function () {
    var w = process.stdout.write.bind(process.stdout);
    return function processStdoutWrite (s) { w(s + "\n") };
}();

var count = createCountStream(16);
count
  .pipe( createFizzBuzzStream())
    .pipe( process.stdout);
count.resume();

/* 結果
 * 1
 * 2
 * Fizz
 * 4
 * Buzz
 * Fizz
 * 7
 * 8
 * Fizz
 * Buzz
 * 11
 * Fizz
 * 13
 * 14
 * FizzBuzz
 * 16
*/

fizzbuzz-stream.js を見ればわかることだけど、このストリームは、単純に値を変換しているだけのストリームなので、FizzBuzz 以外の要件でも _write 関数の中身を書き換えるだけで、色々応用出来るのが嬉しいですね。


count-stream.js

var stream = require('stream');
var util   = require('util');

function CountStream (max) {
    stream.Stream.call(this);

    if (typeof max !== 'number'   ||
        parseInt(max, 10) !== max ||
        max <= 0
    ) {
        throw new TypeError('"max" must be "Integer Number" and over "0"');
    }

    this.max      = max;
    this.count    = 0;
    this.readable = true;

    this.once('close', function () {
        this.readable = false;
        this.emit('end');
    }.bind(this));
}
util.inherits(CountStream, stream.Stream);

CountStream.prototype.resume = function () {
    var that = this;
    var count = function () {
        process.nextTick(function () {
            if (that.paused || ! that.readable) return;
            if (that.count >= that.max) return that.destroy();

            that.emit('data', (that.count += 1).toString());
            count();
        });
    };

    delete this.paused;
    count();
};
CountStream.prototype.pause = function () {
    if (! this.readable) return;
    this.paused = true;
};
CountStream.prototype.destroy = function () {
    if (! this.readable) return;
    this.emit('close');
};

module.exports.CountStream = CountStream;
module.exports = function (count) {
    return new CountStream(count);
};

ReadableStreamのpipeの挙動をテストする

先日の TDDBC1.0 の写経お題「FizzBuzz」でしたが、JSer(というか NodeJSer)なら Stream 実装するのも1つのやり方だと思います。

readableなStreamで「1, 2, Fizz, ...」というデータを流して、writableなStream(例えば、process.stdout)で表示させる、みたいな。

書き方としては、こんな書き方ができると思う。

(new FizzBuzzStream(finish_number)).pipe(process.stdout);

と言ったところで、本題。

問題は、FizzBuzzStream モジュールを開発しようとする場合、当然 pipeを通した時の挙動が期待したものかをテストしなくちゃいけないんだけど、どうするのがいいんだろう?

なかなかベターな解答が思い浮かばないので、readableStreamが(pipeを使って)writableStreamをハンドルした時、流れてくるデータを 外部に書きだす代わりに バッファに書き込んで、適時チェックするようにした。

その時に、内部のバッファに書き込む テスト用のWritableStream

TDD Boot Camp 長岡 1.0 に行ってきました #tddbc

関連リンク

主催者の @masaru_b_clさん。講師 @t_wada さん。ならびに TA、参加者、スタッフの皆さんお疲れ様でした。

スタッフ(タイムキーパー)参加です。詳しい内容については リンク先を参照で。


箇条書きで。

  • TAによるペアプログラミングの実演(& @t_wada さんの解説)に沿って、参加者が実演内容を「写経」する時間が設けられていた => TDD未経験者・初心者にとっては TDD導入の障壁が1つ取り除かれていた
  • ペアプログラミング。ペアを組む相手がいるので、一人だと気づかないテストケースがどんどん出てくるように見えた => 設計で時間が掛かるケースもあるのかな? という感想
  • タイムキーパーの仕事優先のため、僕はぼっちTDDしてました
  • レビュータイム。言語とフレームワークは冒頭に発表することを約束事にしておいたほうがいい気がした。レビュー発表で何をしたらいいのか迷ってしまったという発表者もいたので、テストコードで注意した点。実装コードを書く上で気にしたこととか、リファクタリングは何をどうしたのかとか。


ちなみにぼっちTDDの成果物 -https://gist.github.com/ishiduca/5607210
追記 (2013.5.22) github に移した https://github.com/ishiduca/tddbc_nagaoka_java_version_parser

  • 言語: JS(CommonJS)
  • フレームワーク: qunit(qunit-tap)
  • .parse() が 同じ正規表現を2度通過させてるのがダサい
  • anothor.constructor === this.constructor で同じ Version コンストラクタから生成されたオブジェクトかどうかの判断してるけど、実は不安(なので本当はここを徹底してテストすべきだったかな、と)
  • 正直、可読性がまだまだ良くない
  • new 演算子使わないで Object.create でやれば、イミュータブルなオブジェクトにできてよかったかなと。
  • アップデートのテスト増やさないと不安

まだまだですが、継続重要らしいので、もう少し見直し

クライアント・サーバーでバリデーションの仕組みを共有したい

https://github.com/ishiduca/js-valid

  • サーバーとクライアントの両方で同じバリデータとスキーマを使いたい
  • 型チェックは既存のライブラリ(お好みの)に任せたほうが楽(な気がする)

例えば、undersocore.js のようなサーバーからもクライアントからも同じリソースを参照できるライブラリがあるので、それ使うことが許される環境なら、スキーマも両方から参照できるところに配置しておけば楽だなーとか。

schema.js

(function (g) {
    var underscore = g._ ? g._ : (function () {
        return require('./underscore');
    })();

    var schema = {
        foo: underscore.isString
      , bar: {
            type: underscore.isString
          , required: true
            varidate: /^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/
        }
      , count: {
            type: function isInt (v) {
                return underscore.isNumber(v) && n === parseInt(n);
            }
          , default: 0
        }
    };

    if ('undefined' !== typeof module && module.exports &&
        'function' === typeof require
    ) {
        module.exports.schema;
    } else { // this === window
        g.schema = schema;
    }
 })(this);


app.js

var Validator  = require('js-valid').Validator;
var underscore = require('./underscore');
var schema     = require('./schema');

var validator = new Validator(schema);

method({ foo: '100', bar: 'fo.o@bar.org' });
// { foo: '100', bar: 'fo.o@bar.org', count: 0 }

method({ foo: '100' });
// Error: RequiredError: "bar" not found

function method (query) {
    var validated;
    try {
        validated = validator.validate(query);
    } catch (e) {
        console.error(e.name + ': ' + e.message);
    }
    return validated;
}

クライアントサイドでも同じような呼び出し方出来る

var validator = new some.Validator(schema);
...

、、、と、ここまできたら、通信する際の シリアライズとデシリアライズ機能あると楽ですよね?

+---(クライアント)---+
|                    |
|  beforeValidateObj |
|         |          |         -+
|     [validate] -> エラー処理  |
|         |          |          +-- validator.stringify(beforeValidateObj)
|  afterValidateObj  |          |
|         |          |          |
|    [serialize]     |          |
|         |          |         -+
|       string       |
+---------+----------+
          |
       HTTP通信
          |
+-----(サーバー)-----+
|       string       |
|         |          |         -+
|   [deserialize]    |          |
|         |          |          +-- validator.parse(string)
|  deserializedObj   |          |
|         |          |          |
|     [validate] -> エラー処理  |
|         |          |         -+
|  afterValidateObj  |
|                    |
+---------+----------+

、、、この辺の仕組みスマートにやりたいな

オブジェクトのキー(プロパティ名)に関数

知らなかったんだけど、常識なんですか?

var dbl = function (n) { return n * n; };
var o = {};

o[dbl] = 2;

console.log(dbl(o[dbl]));
// 4

追記

しらんかった

Niigata.LL で JSでTDDをどう実践しているのか をトークしました

ポータルサイトの方を見てもらうとわかりますが、VBAとかPowerShellとかもあって面白かったし、飛び入りで来てトークしてくれた方もいて面白かったし、PHPを話す人が居なかったのはなにか陰謀があったんじゃねーのかとか妄想できるし、濃密で良かったですよ。


トークタイトル:「JS TDD(and test) with your language

Webアプリを作っていると得意(メイン)の言語とは別に JavaScript に触れざるを得ないプログラマは多いんじゃない?
という観点で、非JSer対象にJSのTDD周りの話を。

※ 余談ですが、Niigata.LL では 18名の方の参加がありましたが、主にJavaScriptを書いているというJSerは2名ほど。開催日現在で Node.js 使っている方は 1名という状況で、思った以上に JSer いない! kanazawa.js だとあんなに女の子いっぱいいたのに! ひどい! とか思ったもんです。

念頭に置いた事など

  • Node.js のインストールを前提としない
  • ブラウザ上で動くJSを対象とした
  • 快適に TDD サイクルを回すことを目的に
  • 単体(ユニット)テストを対象とした
  • コマンドライン上でのテストを可能にする
  • モジュール(ファイル)の更新に合わせて、自動的にテストが走るようにする

Step 1. ブラウザ上でテストする

use QUnit

フレームワークには QUnitを チョイスしました。Test::More に似たフレームワークなので、Perl経験者には扱いやすいので。(RSpecなんかに馴染んでるRubyの人とかは、Jasmine あたりがいいんじゃないのかと。あと Mocha あたりもいいんじゃないかと)

ディレクトリ構造
  • Dancer とかの軽量のWAFだと、スタティックなファイルを配信するディレクトリが public/*/ とか多いので、開発対象となるモジュールファイルは public/js/ 配下においてみた。
  • テストに使う QUnit ライブラリは t/qunit/ 配下に置いた。ユーザーからはリーチしない
  • テストを記述したファイルは t/ 配下に。(この例は t/test.js)
  • テスト用のHTML は t/ 直下に。(この例だと t/index.html)
MyProject/
  |-- public/
  |   `-- js/
  |       `-- mod.js // テスト対象となるJSファイル
  |-- run-phantom.js // PhantomJS で テスト用HTMLを走らせるヘルパー
  `-- t/
      |-- index.html // テスト用HTML
      |-- qunit/
      |   |-- qunit-1.10.0.css
      |   `-- qunit-1.10.0.js
      `-- test.js    // テストを記述したファイル
QUnitライブラリのダウンロード
$ curl -O http://code.jquery.com/qunit/qunit-1.10.0.js
$ curl -O http://code.jquery.com/qunit/qunit-1.10.0.css

※ 2013.3.23 現在で qunit の最新版は 1.11.0 です。が、後ほどQUnit-TAPを使用する場合、1.11.0 だとうまくない場合があるので、1.10.0 を利用します。

テスト用HTMLの中身(t/index.html)

<!doctype html>
<head>
<meta charset="utf-8" />

<!-- テストフレームワークqunit.js & qunit.css -->
<link rel="stylesheet" href="./qunit/qunit-1.10.0.css" />
<script src="./qunit/qunit-1.10.0.js"></script>

<!-- テストを受けるモジュール-->
<script src="../public/js/mod.js"></script>
<!-- テスト本体 -->
<script src="./test.js"></script>

<title>TDD with QUnit</title><!-- title は記述することが強く奨励される -->
</head>

<body>
  <div id="qunit"></div><!-- テストの結果を表示する -->
  <div id="qunit-fixture"><!-- DOMテストする場合のフィールド、ここに書いたDOMはテストの度にinitされるので楽が出来る -->
  </div>
</body>

テスト対象のモジュールJS(public/js/mod.js)

function NyanError (message) {
    this.message = message;
    this.name    = 'NyanError';
}

function WannError (message) {
    this.message = message;
    this.name    = 'WannError';
}

(function () {
    NyanError.prototype = new Error;
    WannError.prototype = new Error;
})();

テストJS(t/test.js)
詳しいAPIは 「QUnit API Documentation」を参照してください

$(document).ready(ALL_TEST);

function ALL_TEST () {
    test('Assert', function () {
        var hoge = {hoge: 'HOGE'};

        deepEqual(hoge, {hoge: 'HOGE'}, JSON.stringify(hoge));

        equal(1, 1,   '1 == 1');
        equal(1, "1", '1 == "1"');

        strictEqual(hoge, hoge, JSON.stringify(hoge));
        strictEqual(1, 1, '1 === 1');
        //this error
        // strictEqual(1, "1",'1 === "1"');

        notDeepEqual(hoge, {hoge: /hoge/}
          , JSON.stringify({hoge: /hoge/.toString()}));

        notEqual(1, 2, '1 != 2');

        notStrictEqual(1, "1", '1 !== "1"');

        ok(true, 'true');

        // Error Test
        // 以下、public/js/mod.js の関数を使う
        throws(
            function () { throw new NyanError('にゃーーーーーん!!!'); }
          , NyanError // <- instanceof NyanError
          , 'raized error is an instance of "NyanError"'
        );

        throws(
            function () { throw new WannError('ワン!!!!!!!'); }
          , /ワン/ // <- 正規表現
          , 'raized error is an instance of "WannError"'
        );
    });

    test('AsyncTest - 非同期テスト', function () {
        stop();

        setTimeout(function () {
            ok(true);
            start();
        }, 500);
    });
}

テストの実行

$ open t/index.html

Step 2. CLI上でテストする

use PhantomJS
  • PhantomJS は 画面描画しないブラウザ(ヘッドレスブラウザ)
  • QtWebKitベースで、HTML5とかCSS3とか解析できる
  • でも画面キャプチャはできる

install PhantomJS

適当なディレクトリに http://phantomjs.org/download.html からバイナリをダウンロード & 展開

$ bin/phantomjs --version してバージョン確認出来ればOK

setup PhantomJS

# パスの確認
$ echo $PATH | perl -wpl -e 's/:/$&\n/'
# bin/phantomjs のシンボリックリンクをパスの通っているディレクトリに張る
$ sudo ln -s /Users/ishiduca/phantomjs/bin/phantomjs /usr/local/bin/phantomjs
# パスの確認
$ phantomjs --version

QUnit を PhantomJS から使うための runner.js を用意する

curl -O https://raw.github.com/jquery/qunit/master/addons/phantomjs/runner.js
mv runner.js run-phantom.js

PhantomJSでのテスト

$ phantomjs run-phantom.js t/index.html

# 結果
Took 604ms to run 12 tests. 12 passed, 0 failed.
# 12テスト中 成功: 12 失敗: 0

失敗すると

Test failed: undefined: Assert
    Failed assertion: raized error is an instance of "WannError", expected: null, but was: WannError: ワン!!!!!!!
    at file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:532
    at file:///Users/ishiduca/MyProject/t/test.js:37
    at file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:136
    at file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:279
    at process (file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:1277)
    at file:///Users/ishiduca/MyProject/t/qunit/qunit-1.10.0.js:383
Took 543ms to run 12 tests. 11 passed, 1 failed.

な感じで出る

Step 3. CLI環境でテストの詳細を出力する

use QUnit-TAP
  • コマンドライン向けに各々のテストの結果を出力するプラグイン
  • QUnit単体では標準出力する機能がないので、その部分を補完する
  • TAP形式で出力する
  • 作者が日本の技術者なので日本語で質問できる!

install QUnit-TAP

$ curl -O https://raw.github.com/twada/qunit-tap/master/lib/qunit-tap.js

出力先を指定するコードを t/index.html へ追加記入(<script src="./qunit/qunit-1.10.0.js"></script>の後に)

<script src="./qunit/qunit-tap.js"></script>
<script>
qunitTap(
    QUnit
  , function () { console.log.apply(console, arguments) }
);
// QUnit.config.autostart = false; // AMD形式で非同期でモジュールを読み込むときは必要
</script>

実行

$ phantomjs run-phantom.js t/index.html
# test: Assert
ok 1 - {"hoge":"HOGE"}
ok 2 - 1 == 1
ok 3 - 1 == "1"
ok 4 - {"hoge":"HOGE"}
ok 5 - 1 === 1
ok 6 - {"hoge":"/hoge/"}
ok 7 - 1 != 2
ok 8 - 1 !== "1"
ok 9 - true
ok 10 - raised error is an instance of TypeError
ok 11 - raised error message contains "type_error"
# test: AsyncTest - 非同期テスト
ok 12
# module: form data validator test
# test: ok pattern
ok 13 - form data validate test passed
# test: error pattern
ok 14 - account not pass test
1..14
Took 599ms to run 14 tests. 14 passed, 0 failed.

TAP形式で出力できるので TAPを対象にしたツールを利用できるようになるので、以下応用編!

Step 4. use prove

  • Perlプロダクトのコマンドラインツール
  • 基本的には t/以下のディレクトリのテストを実行する
  • --exeオプションを付けることで perl 以外のテストも実行できる
  • 結果をカラフルに(グリーン・レッド)
  • ex prove --exe node --ext .js -v

prove に食わせる test.sh を用意する

#!/bin/sh
phantomjs run-phantomjs $PWD/t/index.html

実行

$ prove -v --timer test.sh
[22:06:54] test.sh .. 
# test: Assert
ok 1 - {"hoge":"HOGE"}
ok 2 - 1 == 1
ok 3 - 1 == "1"
ok 4 - {"hoge":"HOGE"}
ok 5 - 1 === 1
ok 6 - {"hoge":"/hoge/"}
ok 7 - 1 != 2
ok 8 - 1 !== "1"
ok 9 - true
ok 10 - raised error is an instance of TypeError
ok 11 - raised error message contains "type_error"
# test: AsyncTest - 非同期テスト
ok 12
# module: form data validator test
# test: ok pattern
ok 13 - form data validate test passed
# test: error pattern
ok 14 - account not pass test
1..14
Took 572ms to run 14 tests. 14 passed, 0 failed.
ok     1961 ms
[22:06:56]
All tests successful.
Files=1, Tests=14,  2 wallclock secs ( 0.03 usr  0.01 sys +  0.53 cusr  0.14 csys =  0.71 CPU)
Result: PASS

実際は All tests successful のところがグリーンで表示されるのが気持ちいいです。

TDDによる開発の場合、素早くそのサイクルを回すことが肝要。なので、コード修正に合わせて(ファイルの更新に合わせて)テストを自動実行できる方が楽なので、そのためのツールを用意します。

Step 5. use pfswatch

  • Perlプロダクトのコマンドラインツール
  • 監視したいディレクトリやファイルの更新に合わせて任意のコマンドを実行する

監視の開始

$ pfswatch public/js/ -e prove -v --timer test.sh

public/js/以下のファイルが更新される度に、test.sh が走ってテスト結果を表示してくれます。

おまけ。

テストの結果を音声で通知させます。
僕の開発環境はMacBook13incなわけですが、スクリーンを2分割して片方にテストコードを表示させて、もう片方でモジュールのコードを書いているんですが、自動実行しているテストは裏で走らせてるので、結果が成功したのか失敗したのか通知してくれる仕組みがないと、ファイルの更新ごとにテスト結果を見に行かないといけない。これはよくない! ということで、今回は結果通知を音声に頼ってみました。

Step 6. use with-sound

  • Perlプロダクトのコマンドラインツール
  • コマンドが成功するか失敗するかによって、その結果に対応した音声が流れるアプリケーション

実行

$ with-sound phantomjs run-phantom.js t/index.html
# とか
$ pfswatch public/js/ -e with-sound prove -v --timer test.sh

まとめっぽいこと言う

慣れ親しんだ言語文化の資産を横流しすると便利

今回のトークでは テストフレームワークでは QUnit を使いました。
上記にもありますが Perlのテストフレームワーク Test::More に似たフレームワークだったからですし、Perlプロダクトの prove pfswatch with-sound というコマンドラインツールを使ってTDDを捗らせる工夫をしました。
けど別に Perl がいいよっていう話ではなく、自分の得意な言語文化を下敷きにするのもいいんじゃないのかなーと。そんな気分を話してみました。


最後に、Perl いいですよ

モジュール側(呼び出し先)から実行スクリプト(呼び出し元)のディレクトリのパスを獲得する

こういうのを
./app.js

var path = require('path');
var waf  = require('w-a-f');
var wf   = waf();

wf.use(require( __dirname + '/middleware/warn' )())
  .use(require( __dirname + '/middleware/nyan' )())
  .use(require( __dirname + '/middleware/guu'  )())
;

こういうふうにしたい

var path = require('path');
var waf  = require('w-a-f');
var wf   = waf();

wf.use('warn')
  .use('nyan')
  .use('guu' )
;

呼び出されるモジュールから呼び出し側のスクリプトのパスが認識できれば、上のような省略可能だけど、呼び出されるモジュールで __dirname は使えない。ので

./node_modules/w-a-f/index.js

var dirRunScript = path.dirname( process.argv[1] );
var middleware   = require(path.join( dirRunScript, 'middleware', middlewareName ))();

で取得する。

|-- app.js * 実行スクリプト
|-- middleware
|   |-- guu.js
|   |-- nyan.js
|   `-- wan.js
`-- node_modules
    `-- w-a-f
        |-- index.js * 呼び出されるモジュール

実際のコードはここにある

もっとスマートな方法あるのかな?