JSON 形式で設定ファイル書かせるの、やめてもらえません?
設定ファイルを JSON 形式で記述するライブラリやフレームワークって、
- PHP のパッケージ管理システム composer の composer.json
- node のモジュール管理システム npm の package.json
- クライアントサイド JavaScript のパッケージ管理システム bower の bower.json
- MacOSX のテキストエディタ Sublime Text の設定ファイル
などなどいくつかあるけど(例が偏っててすみません)、
JSON って文法が結構デリケートなので、人間が(読む分にはいいけど)書くのには適してないと思うんですよね。
1. コメントが書けない!
JavaScript みたいにコメント書けません。
... "devDependencies": { //"grunt": "0.4.2", ちょっと退避 "grunt": "0.4.1", // 古いバージョンに戻す "grunt-contrib-watch": "0.5.3", "grunt-contrib-uglify": "0.2.4" }, ...
とか書けさえしたらどれだけ管理が楽になることか!
2. 連想配列やリストの要素を区切るカンマに注意する必要がある
↓は JSON としてはシンタックス違反ですが、どこが間違ってるか分かりますか?
... "devDependencies": { "grunt": "0.4.2", "grunt-contrib-watch": "0.5.3", "grunt-contrib-uglify": "0.2.4", }, ...
答えは連想配列の最後がカンマで終わってるところです。正しくはこう。
... "devDependencies": { "grunt": "0.4.2", "grunt-contrib-watch": "0.5.3", "grunt-contrib-uglify": "0.2.4" // <- 最後に , があっちゃダメ }, ...
これはリストでも同様です。
... "keywords": [ "hoe", "hoehoe", "ehehe" // <- 最後に , があっちゃダメ ], ...
連想配列やリストに要素を追加したり並べ替えたりするのって頻繁にやるのに、その度にいちいちカンマに気を配るのって超めんどくさい。
もちろん、末尾のカンマだけでなく、要素と要素の間のカンマ忘れもありがちなミスですよね。
3. 文字列は必ずダブルクォートで囲まないといけない
JavaScript のオブジェクトリテラルでは
{ "foo": "aaa", // ダブルクォートで囲んだり 'bar': 'bbb', // シングルクォートで囲んだり buz: 'ccc', // ていうか key はクォートで囲む必要すらない }
って書けるけど、JSON では
- 文字列はシングルクォートではなく、必ずダブルクォートで囲まないといけない
- value だけでなく key も、文字列である場合はダブルクォートで囲まないといけない
ので、さっきの例は
{ "foo": "aaa", "bar": "bbb", "buz": "ccc" }
て書かないといけないです。
だからさー
設定ファイルは YAML 形式で書かせて下さいお願いします。。。
上で挙げた例すべて、YAML にするだけで一発解決ですよ。
ライブラリやフレームワークを管理してる方 or これから作るという方はどうぞご検討下さいm(_ _)m
jasmine + スクリーンショット作成サービスでWebアプリを複数ブラウザで一括動作確認!
この投稿は JavaScript - Client Side - Advent Calendar 2013 の13日目の参加記事です。
JavaScript のテスティングフレームワーク jasmine でテストを書いておくと、コードをいくらいじってもブラウザのリロード一発で動作確認できてそれだけでもステキです(例)。
でも更に、「スクリーンショット作成サービス」と組み合わせて利用すれば、複数の OS の複数のブラウザで一気にまとめて動作確認できてしまってステキングです。
このエントリでは、
- スクリーンショット作成サービスをいくつか紹介して、
- それら + jasmine による一括動作確認の方法について書いて、
- 最後に実際にやってみた例を紹介
します。
それではGO。
スクリーンショット作成サービス
を、2つ+αほど紹介。
1. browserstack.com の Screenshots
動作確認したいページのURLを入力して、めぼしいブラウザを選択して(デフォルトで20ブラウザ選択済、最大25まで選択可)、画面一番下の "Generate screenshots" をクリックすると、
こんな風にぞくぞくとスクリーンショットが送られてくる。
対応OS、対応ブラウザが盛りだくさんでステキ。なにより、スマホ対応なサービスってここしかないかも。
縦に長いウェブページも、画面の上から下までスクロールして撮ってくれます(確認した限り、多分全ブラウザで)。
欠点は、一度に25ブラウザまでしか選択できない点。(月額払えばこの制限はなくなる??)
また、無料で撮れるスクショ数はトータル100件までです。動作確認するブラウザを絞ってうまく利用しないといけない。Screenshots だけの利用であれば、月額$19でこのリミットはなくなるみたいです。
2. browsershots.org
無料アカウント登録さえすれば、ブラウザ数の制限なしで、一気にまとめてスクショが撮れちゃうのがステキすぎる。アカウント登録なしでも1日あたり100ブラウザ?くらいまでなら多分利用可能。
Linux や BSD 対応ってのも、動作確認するサービスによっては重要でしょう。
撮ったスクショには有効期限があり、無料アカウント登録すれば30分、アカウントなしなら14分。
欠点としては、スクショを撮るホスト&ブラウザの提供を有志に頼っているため、対応ブラウザがちょこちょこ変動します。ついこないだは Mac の対応ブラウザは Safari が2つあったはずなのに、今みたら Mac の対応ブラウザ数ゼロになってた。。。
また、僕が見てる限り Mac の登録ブラウザ数が少ないです。(これはたまたまなのか、常にこうなのか?)
あと、スクロールの必要なページの場合でも、画面の1番上だけしか撮ってくれません。
とはいえ、Windows の対応ブラウザの多さと、無料でこれだけ多数のブラウザのスクショをまとめて一気に撮れるというのは強いです。
ちなみに、画面下の方のボタンで Windows ブラウザのみ全て選択、とか出来ます。
3. その他のサービス
screenshots.jp
月額払わないとスクショの撮れるブラウザが少なすぎるので試してないです(´・ω・`)
主要OSの主要ブラウザは対応しているようです。
browsershots.at
撮れるのは Windows Vista の Firefox のみ(´・ω・`)
jasmine のテスト結果をまとめてスクリーンショット!
さて、
これらスクリーンショット作成サービスで jasmine のテスト結果のスクショを撮れば、
色んな環境でズガッとまとめて動作確認出来ちゃうぜ!というのが今回のお話。
ブラボー!おお・・・ブラボー!!
ただ、jasmine のテスト結果って画面一番下に表示されるから、1画面に収まるようなコンパクトなWEBアプリケーションでもない限り、スクショ一覧画面のサムネイルでは確認出来そうにないです。
いちいちサムネイルをポチポチクリックして、個別の画像を開いて、画像の一番下までスクロールして確認しないといけない。それってめんどくさい。(ていうか browsershots.org の場合は画面1番上しか撮れないのでそれすら無理。)
どうせなら、スクショ一覧画面でサムネイルを眺めるだけでテスト結果を確認したいですよね。
じゃー CSS or JavaScript で、jasmine のテスト結果を画面1番上に移動しちゃえばいいんじゃない?
jasmine テスト結果を画面1番上に表示させるには
jasmine-1.3.1 と jasmine-2.0.0-rc5 とで調べてみました。それぞれ、テスト結果を収めてるDOM要素がちょこっと異なります。
jasmine-1.3.1 の場合
テスト結果は id="HTMLReporter"
な div に収められてます。
CSSでやるなら
#HTMLReporter { position: absolute; top: 0px; width: 100%; background-color: white; }
jQuery でやるなら、
jasmineEnv.execute();
を呼んでる箇所の直後に
$('#HTMLReporter').remove().prependTo('body');
でオッケー。
jasmine-2.0.0-rc5 の場合
テスト結果は class="html-reporter"
な div に収められてます。
CSSでやるなら
.html-reporter { position: absolute; top: 0px; width: 100%; background-color: white; }
jQuery でやるなら、boot.js の下の方にテストをキックしてる箇所があるので、その直後に以下のように1行追加してやればいける。
// ... window.onload = function() { if (currentWindowOnload) { currentWindowOnload(); } htmlReporter.initialize(); env.execute(); // <-jasmineテスト実行 // 以下を追加 $('div.html-reporter').remove().prependTo('body'); }; // ...
実際にスクショを撮ってみた
拙作の jQuery プラグイン jquery.narrows.js のサンプルページ兼テストページ(の、テスト結果を一番上に移動したversion)で実験してみました。
まずは Screenshots。
サムネイルだけ眺めても大体分かりますよね?サムネイルのてっぺんに緑のバーが出ていれば成功、赤のバーならテスト失敗です。
- Windows 7 の IE8(中段左から2番目)で赤いバーが出てるのが見える。テスト失敗してますがな。 元サイズの画像を開いて、エラーの内容も確認出来た。
- Amazon Kindle Fire 2, Amazon Kindle Fire HD, Google Nexus 7 では、23個のテストすべて終了する前の時点のスクショが撮れてしまってた(そのため青っぽいバーが見えてます)。ひょっとして Kindle や Nexus は JavaScript の実行速度が遅いんでしょうか? iPhone や iPad はちゃんと成功してるのにな?
- 一番最後の iPad 3rd (7.0)(びっくりマークのやつ)はタイムアウトした模様。ブラウザが多すぎると、最後の方のブラウザはこんな風にタイムアウトしてしまうっぽい。
- IE6, IE7(上段右端、中段左端)だけ、テスト結果がどこにも表示されてませんでした…。JavaScript がオフなのか、jasmine が実行できてないのか、それともさっき追加した CSS がいけないのか、不明。
次に browsershots.org。Windows の全ブラウザのみ選択して、試してみました。
105ものブラウザのスクショをリクエストしても粛々と任務遂行してくれるこの度量の深さ。ありがたいことです。(ここに表示してるのは一部のみです。)
- MSIE 8.0, Firefox 1.0.8, Netscape 8.0.4, Netscape 7.1 でエラーを確認。 MSIE 8.0 でのエラーはさっきの結果と矛盾しません。それ以外のブラウザでのエラーは単に古すぎるためのようです。
- いくつかのブラウザで、スクショ作成サーバの起動か何かに失敗していて、スクショが正常に撮れていませんでした(デスクトップにコマンドプロンプトだけ表示されてるやつとかです)。有志さん頼りな以上、こういうエラーはある程度仕方ないんでしょう。
こんな感じでした。なんだか色んなケースが確認出来てこれはいいサンプル(自画自賛)。
そして期せずして、jquery.narrows.js が IE8 で動作しないことが分かっちゃいました。直さなくちゃ…。僕Windowsマシン持ってないので、これ、そうそう気付けないバグですよ。
すごくない?(・ω・)-3
まとめ
みんな jasmine でテスト書こうよ。いいから書こうよ。
jasmine のテストをページロード時ではなくボタンクリックで実行するには
jasmine テストに時間かかりすぎ!キー!
あるプロジェクトで、600行の JavaScript コードのテストを jasmine で書いたら、テストコードだけで700行を超えてしまいました(笑)
で、テストにかかる時間も、およそ30〜40秒。長い! コードでもテストでもDOM要素をいじりまくってるせいなんですが。(select を選択したり checkbox をチェックしたり)
リロードの度に毎回ここまで時間がかかるようになると、キーッってなります。
(゚皿゚)キー!
自動実行しなきゃいいじゃん
なので jasmine テストをページロード時に自動で実行させる代わりに、ボタンをクリックして手動実行する方法を考えました。
jasmine-1.3.1 の場合
jasmine 1系では、jasmine の初期化・自動実行する部分をライブラリ同梱の SpecRunner.html
からコピって使ってると思います。ここを修正すればよし。
ページロード時の自動実行部分をコメントアウトして、代わりに jasmine.execute
ってメソッドを追加して、こいつでテストを手動実行出来るようにします。
(jasmine.execute
って名前はなんでもよいです。 jasmine.hogehoge
でも、なんなら window.hogehoge
ってグローバルに置いちゃうのも可。)
(function() { var jasmineEnv = jasmine.getEnv(); jasmineEnv.updateInterval = 1000; var htmlReporter = new jasmine.HtmlReporter(); jasmineEnv.addReporter(htmlReporter); jasmineEnv.specFilter = function(spec) { return htmlReporter.specFilter(spec); }; var currentWindowOnload = window.onload; // 1. ページロード時の自動実行をコメントアウト // window.onload = function() { // if (currentWindowOnload) { // currentWindowOnload(); // } // execJasmine(); // }; // // function execJasmine() { // jasmineEnv.execute(); // } // 2. jasmine.execute ってメソッドを追加 jasmine.execute = function () { jasmineEnv.execute(); } })();
そしたら、HTML中に手動実行するためのボタンを配置して、
<button id="run-jasmine-test">jasmineテスト実行</button>
このボタンのクリック時にテストを実行するようにしてやればOK。
$('#run-jasmine-test').on('click', function () { if (confirm('jasmineテストを実行します。よろしいですか?')) { var $button = $(this); // ボタンをdisable $button.attr('disabled', 'disabled').text('実行中...'); // 「実行中...」が表示されるように一瞬起動を遅らせる setTimeout(function () { // さっき定義した jasmine.execute をコール jasmine.execute(); // テスト終了! $button.removeAttr('disabled').replaceWith('テスト完了'); }, 100); } });
jasmine-2.0.0 の場合
jasmine 2系からは、jasmine の初期化・自動実行は lib/jasmine-2.0.0/boot.js
が担当するようになりました。
この boot.js
の先頭のコメントに「このファイルは好きなようにいじってね (this file can be customized directly)」とあるので、じゃーカスタマイズしちゃいましょ。
このファイルの下の方に window.onload
で jasmine を起動する部分があるので、コメントアウトして、さっきと同じように jasmine.execute
ってメソッドでテストをキック出来るようにします。
// ... // 1. ページロード時の自動実行をコメントアウト // /** // * ## Execution // * // * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. // */ // var currentWindowOnload = window.onload; // // window.onload = function() { // if (currentWindowOnload) { // currentWindowOnload(); // } // htmlReporter.initialize(); // env.execute(); // }; // 2. jasmine.execute ってメソッドを追加 jasmine.execute = function () { htmlReporter.initialize(); env.execute(); }; // ...
あとは 1.3.1 の場合と同様、HTML中にボタンを配置して、ボタンクリック時にテストを実行するようにしてやればOKです。
これで
jasmine テストがとんでもなく遅くなっても心穏やかに開発出来るようになりました^^
5分で分かる jasmine テストフレームワーク
JavaScript のテストフレームワーク jasmine がゴキゲンだぜ。書きやすいしテスト結果も見やすいしかわいいよちゅっちゅ。
そんな jasmine ちゃんをまだ触ったことない人のために、出来るだけ取っ付き易くなるように解説してみたいと思います。拙作の jQuery プラグイン jquery.narrows.js のテストでも使っておりますので、これをサンプルとして説明していきます。
使用するバージョンは jasmine 2.0.0 RC5 スタンドアローン版。2系はまだRC版ですが、使っていて特に不具合に出くわしたことはないです。2系になって大きく改善してる部分もあるので、アグレッシブに使っていきましょう。(Rubyフレームワーク用の gem 版(jasmine-gem)というのもありますがここでは触れません)
まずは見てみよう
とりあえず実際に動くものを見てみましょう。jquery.narrows.js のサンプルページ兼テストページがこちら。 → sample.html
画面の一番下までスクロールすると、こんな↓感じに jasmine のテスト結果が表示されてるのが見えるかと思います。
- food は初期状態で disabled であるべき
- category で "meat" を選択したら food は肉に絞り込まれるべき
- category で value="" を選択したら food は disabled であるべき
- などなど…
といったテストを実行して、全て成功しています。
ちなみにテストに失敗した場合の表示はこんな感じ。
jasmine 導入方法
導入の仕方について。
公式のダウンロードページには現在バージョン1系しか置いてなくて、2系は master ブランチの "dist" ディレクトリからダウンロードできます。 現時点での最新版は jasmine-standalone-2.0.0-rc5.zip です。
ダウンロードして解凍すると、ファイル構成はこんな感じ。
├── MIT.LICENSE ├── SpecRunner.html ├── lib │ └── jasmine-2.0.0-rc5 │ ├── boot.js * │ ├── console.js │ ├── jasmine-html.js * │ ├── jasmine.js * │ ├── jasmine.css * │ └── jasmine_favicon.png ├── spec │ ├── PlayerSpec.js │ └── SpecHelper.js └── src ├── Player.js └── Song.js
このうち、ブラウザ上でのテスト実行に必要なのは * を付けたものだけです。これを自分のプロジェクト以下に置いて、HTML に読み込ませます。
<link rel="stylesheet" href="lib/jasmine-2.0.0-rc5/jasmine.css"> <script src="lib/jasmine-2.0.0-rc5/jasmine.js"></script> <script src="lib/jasmine-2.0.0-rc5/jasmine-html.js"></script> <script src="lib/jasmine-2.0.0-rc5/boot.js"></script> <script src="spec/narrowsSpec.js"></script> <!-- テストファイル -->
js ファイルは読み込む順番が決まっており、上に書いたとおり
- jasmine.js
- jasmine-html.js
- boot.js
- テストファイル(ここでは
narrowsSpec.js
)
の順にして下さい。
これだけで、さっき見たようなテスト結果が画面下に表示されるようになります。
テストの書き方
じゃあ実際のテストはどんな風に書くのか?
サンプルで実行しているテストファイルはこちら。 → narrowsSpec.js
jasmine は Ruby の RSpec などの記法を踏襲しているので、RSpec などを使ったことのある人には馴染みやすいと思います。
抜粋して解説します。
describe('例1', function() { // select 要素を jQuery オブジェクトとして取得しておく var $categorySelect = $('#ex1-food-category'); var $foodSelect = $('#ex1-food'); // 各テストの最初に全ての select の選択状態を初期化 beforeEach(function() { $categorySelect.val('').trigger('change'); $foodSelect.val('').trigger('change'); }); it('food select は初期状態で disabled', function () { expect($categorySelect.is(':disabled')).toBe(false); expect($foodSelect.is(':disabled')).toBe(true); }); it('category select で "meat" を選択したら food select は肉に絞り込まれる', function () { // category select で "meat" を選択 $categorySelect.val('meat').trigger('change'); // food select の disabled が解除されたことを確認 expect($categorySelect.is(':disabled')).toBe(false); expect($foodSelect.is(':disabled')).toBe(false); // food select が絞り込まれた結果、value="" でない option が最低1つ以上あることを確認 expect($foodSelect.find('option[value!=""]').size()).toBeGreaterThan(0); // food select の option の data-ex1-food-category 属性が全て "meat" であることを確認 $foodSelect.find('option[value!=""]').each(function () { expect($(this).data('ex1-food-category')).toBe('meat'); }); }); });
describe は、複数のテストをまとめる働きをします。
describe('例1', function() { //... });
階層構造にすることも出来ます。
describe('例1', function() { describe('例1-1', function() { //... }); //... });
この describe の中に、テスト本体である it や、前処理の beforeEach、後処理の afterEach を書いていきます。
beforeEach は名前の通り、各テスト毎に実行させたい前処理を書きます。
beforeEach(function() { // 前処理 });
afterEach は各テスト毎の後処理。
afterEach(function() { // 後処理 });
it がテスト本体です。テストの中身を記述します。
it('food select は初期状態で disabled', function () { expect($categorySelect.is(':disabled')).toBe(false); expect($foodSelect.is(':disabled')).toBe(true); });
it 中にいっぱい出てくる expect がキモです。
expect($categorySelect.is(':disabled')).toBe(false);
この場合、 $categorySelect.is(':disabled')
の結果が false
であることを期待する(expect)、という意味。
it 中の expect が1つでも失敗すれば、そのテストは失敗扱いとなります。
expect にチェーンしているメソッド toBe は Matcher といいまして、expect に渡した中身が条件にマッチするかを判定します。
様々なバリエーションがあります。
// foo === 1 であるか expect(foo).toBe(1); // foo == 1 であるか expect(foo).toEqual(1); // 文字列が正規表現にマッチするか expect(foo).toMatch(/^[a-z]+$/); // 変数が定義済みか expect(foo.func).toBeDefined(); // 変数が未定義か expect(foo.func).toBeUndefined(); // 変数がnullか expect(foo).toBeNull(); // 変数が true 相当の値か(true, 1, "a" など) expect(foo).toBeTruthy(); // 変数が false 相当の値か(false, 0, 空文字列など) expect(foo).toBeFalsy(); // 配列が値を含んでいるか expect([1, 2, 3]).toContain(3); // foo < 2 であるか expect(foo).toBeLessThan(2); // foo > 0 であるか expect(foo).toBeGreaterThan(0); // 小数が有効数字 0 ケタで 3 に等しいか expect(3.141592).toBeCloseTo(3, 0); // 関数 func が何らかの例外を投げることを期待 expect(func).toThrow();
とりあえず今回はここまで。
Symfony2 + Monolog でのロギングについて(その1)
Symfony2 のロギングでデフォルトで使われてる Monolog について調べたので忘れないうちにまとめておく。 何回かに分けて書きます。今回は Monolog イットセルフについて。
Monolog のバージョンは 1.7.0。
Monolog の構造
Monolog は4つのコンポーネントからなります。
ロガー
コントローラで$logger = $this->get('logger')
って取得するおなじみのあいつ。Logger
クラス。 プロセッサとハンドラをいずれも複数登録できます。ハンドラ
メイン処理担当。ロガーからログ情報を受け取って、プロセッサで前処理して、フォーマッタで成形して、出力する。 プロセッサを複数とフォーマッタを1つ、登録できます。 また、以降のハンドラに処理を続行させるかどうかのフラグ (bubble フラグ) も持ちます。 代表的なStreamHandler
はファイルにログを書き込むハンドラです。プロセッサ
ログ情報の前処理を担当。 例えばIntrospectionProcessor
は、ログメソッドの呼ばれた箇所のファイル名、行番号、クラス名、メソッド名を、ログ情報の extra フィールドに付加します。フォーマッタ
ログ情報の成形を担当。 デフォルトのLineFormatter
は、ログ情報を文字通り1行のログに成形します。
プロセッサはロガーとハンドラどっちにも登録出来て、
ロガーに登録すれば、共通の前処理をハンドラに渡す前にさせることができるし、
ハンドラに登録すれば、そのハンドラ専用の前処理をさせられます。
コンポーネント同士の登録関係を図にすると例えばこんな感じ?
ロガー └ プロセッサ1 │ └ ハンドラ1 │ └ プロセッサ1 │ └ プロセッサ2 │ └ フォーマッタ │ └ ハンドラ2(プロセッサなし) └ フォーマッタ
ログ情報 = record
ログ情報は "record" という名前の連想配列でやり取りされます。 record 1つがログ1行に相当します。
こんな形式です。
$record = array( 'message' => "xxxx", // ログメッセージ本文 'level' => 400, // ログレベルを表す数値。DEBUG なら 100, ERROR なら 400 など 'level_name' => "ERROR", // ログレベルを表す文字列 'channel' => "main", // Logger 自身の名前を表す文字列 'datetime' => new \DateTime(), // タイムスタンプ 'context' => array(), // 追加情報その1。ロガーを呼ぶ側から利用出来る。 'extra' => array(), // 追加情報その2。プロセッサなどが使用。 'formatted' => "xxxx", // フォーマッタによる整形済みの文字列 );
「追加情報」が context
と extra
の2つありますけど
- context は、
$logger->debug("ログメッセージ", array(1, 2, 3))
のようにログメソッドの第2引数として外から渡すことのできる情報 - extra は、プロセッサなどが内部的に追加する情報
です。ちなみに LineFormatter
の場合、どちらも単純に json_encode で文字列化して出力してくれます。
おおざっぱな動作
ログメソッドを呼んだ際、各コンポーネントがどんな風に連携して動作するのか、ざくっと解説します。
ハンドラによって動作は微妙に異なるので、ここでは代表的な組み合わせとして StreamHandler
+ LineFormatter
を想定します。
スタート地点は $logger->debug("ログメッセージ")
ですね。
- (Logger) ログメッセージを元に record 連想配列を作る
- (Logger) record をロガーのプロセッサで前処理
- (Logger) ハンドラを順に実行して record を処理していく
- (Handler) record のログレベルを確認、処理対象でなければ何もせず終了
- (Handler) record をハンドラのプロセッサで前処理
- (Handler) record をフォーマッタで文字列に変換
- (Handler) 文字列をファイルに出力
- (Handler) ハンドラの bubble フラグが false なら、これ以降のハンドラでの処理を停止する
今回はここまで。次回は Monolog を Symfony2 で使う際の設定方法について書く予定。