• 個人情報を持たないWebサービスの設計

    Webサービスを作るにあたってどうしても考えなければならないのが、個人情報の取り扱いについて。メールアドレスや名前をもらっただけでもそれだけでも個人情報になるし、IDやパスワードを与えても個人情報になる。Webサービス自体はとてもおもしろいものなのに、信頼できないサイトだった場合使ってくれないこともしばしばある。

    このめんどくさい個人情報の取り扱いをなんとかしてパスする方法はないものかとこの記事では考えてみることにする。

    個人情報とは何か

    Webサービスを作るにあたって最低限必要なものを上げてみる。

    • お名前
    • メールアドレス
    • ID
    • パスワード
    • セッションID

    最低限、このくらいは必要になるだろう。最後のセッションIDというのは、今ログインしているかどうかを判断するのに使う。クッキーで保持されているもののことです。

    では、この5つの項目をパスすれば、個人情報をもたないWebサービスを作ることができるのではないかと考えてみる。

    コンセプト

    そもそもなぜ個人情報が必要なのかというと、自分専用のページを持つということにつきると思う。マイページ。Webサービス上で自分という存在をアピールすることができることがどんなWebサービスにおいても共通解であると思う。例えば、はてなブックマークも自分がはてブしたりすることで、そのWebページに対してコメントを書き加えられる。チラ裏みたいなことができる。

    そういったものを提供できなくては、Webサービスとして成り立たないのではない。だけども、それをやってしまうと個人情報を持つことになる。ではどうすべきか・・・、はい、考えました。

    限定公開URLという考え方

    ユーザーに対してあなた専用のページを与えるのではなく、URLさえ知らなかければ誰もアクセスできないページを提供するということです。IDやPWなどは一切必要とせず、その限定公開URLをユーザーに対して提供する。しかも、仕様上何個でも限定公開URLを発行できる。ボード

    誰にもURLさえ教えなければ、それを自分専用に使うこともできるし、他の人に共有することもできる。

    限定公開URLにとどまらない

    しかも、URLに限定することも実はなくて、「アイデンティケーションコード」を一つだけ持っていればそのコードに対してAPIによってPOSTしたりGETしたりすることによって、例えばChrome拡張にそのコードを登録するだけで、使えるようなサービスが運用できる。ちなみに、アイデンティフィケーションはIDの略なので、IDCODEという意味になってしまうが、ここはIDとは差別化したいのでそう呼ぶことにした。

    ボード

    この限定公開URLによって運用するWebサービスのことをこの記事では以下「ボード」と呼ぶことにします。限定公開URLがひとつのボードとして提供されるという意味からです。

    問題点

    ただ、ボードを提供するにあたって問題点となるのはやはり、限定公開URLなのにもかかわらず、第三者によって傍受されてしまう危険性があるということ、なので日記サイトとか、個人情報に密接に関わりそうそうなWebサービスはできるだけ避けたほうがいい。HTTPSを使わない場合は、更にそのリスクが高まる。

    ではどんなWebサービスがいいのか

    診断メーカーなどは、名前(ニックネーム)を入力するだけですぐに結果がもらえる。そういったWebサービスであれば、個人情報を持つリスクが最小限に抑えられる。

    OAuthというやり方も

    実はHTTPSを使わくてもある程度個人情報を保護できるやり方があり、OAuthを使ってログインすると、限定公開URLを使わなくてもある程度安全なWebサービスを運用できる。診断メーカーもそのうちの一つ。

  • プロジェクト管理ソリューション「JIRA」を試す

    前から気になっていたプロジェクト管理ソリューションJIRAを試してみようと思います。プロジェクト管理と言えばRedmine等のBTSとかリポジトリ管理とかそんなかんじのものを思い浮かべますが、JIRAもそれと同じようなものであるようです。

    しかも高機能な上に、他のプロジェクト管理ツールからチケットをインポートできる機能も併せ持つ今話題のプロジェクト管理ツールだということですから、これは見逃せません。しかも安い。

    というわけで使ってみた。

    お試しということで体験版をダウンロードします。

    # ダウンロード先
    http://www.atlassian.com/ja/software/jira/download
    
    cd /usr/local/src/
    wget http://www.atlassian.com/software/jira/downloads/binary/atlassian-jira-5.2.4.tar.gz
    tar zxvf atlassian-jira-5.2.4.tar.gz
    cd atlassian-jira-5.2.4-standalone/Code language: PHP (php)

    中身を見てみる

    .
    ├── NOTICE
    ├── README.html
    ├── README.txt
    ├── atlassian-jira
    ├── bin
    ├── conf
    ├── external-source
    ├── lib
    ├── licenses
    ├── logs
    ├── temp
    ├── tomcat-docs
    ├── webapps
    └── workCode language: CSS (css)

    README.txt があるので vim で開いてみます。

    vim README.txtCode language: CSS (css)
    BRIEF INSTALL GUIDE
    -------------------
    
    1. Install Oracle's (formerly Sun's) Java Development Kit (JDK) or
       Java Runtime Environment (JRE) version 1.6 or above:
    
       http://www.oracle.com/technetwork/java/javase/downloads/index.html
    
    2. Set the JAVA_HOME variable to where you installed Java. The Windows
       and Linux installers will do this for you. See the following instructions
       for details:
    
       http://docs.atlassian.com/jira/docs-052/Installing+Java
    
    3. Set your JIRA Home Directory.
       Instructions on how to set your JIRA Home Directory can be found here:
    
       http://docs.atlassian.com/jira/docs-052/Setting+your+JIRA+Home+Directory
    
    4. Run 'bin\\start-jira.bat' (for Windows) or 'bin/start-jira.sh' (for Linux/Solaris)
       to start JIRA. Check that there are no errors on the console. See below for
       troubleshooting advice.
    
    5. Point your browser at http://localhost:8080/
       You should see JIRA's Setup Wizard.
    
    Full documentation is available online at:
    
    http://docs.atlassian.com/jira/docs-052/Installing+JIRACode language: PHP (php)

    なるほど・・・。これは敷居が高そうだ・・。
    とりあえず1番から進めていきます。

    JREのインストール

    JIRAを動かすにはJavaが必要なので、 ここからJDKかJREをダウンロードします。私はJDKを選択しました。

    そしてインストール

    tar xzvf jdk-7u10-linux-x64.gz
    cd jdk1.7.0_10/
    mv jdk1.7.0_10 /usr/local/
    vim $HOME/.bashrc
    
    # 追加
    export  PATH=/usr/local/jdk1.7.0_10/bin:$PATH
    
    #vimを保存して終了し、実行
    export  PATH=/usr/local/jdk1.7.0_10/bin:$PATH
    
    Code language: PHP (php)

    ここまでやって、javaを実行できるか確認します。

    java -version
    java version "1.7.0_10"
    Java(TM) SE Runtime Environment (build 1.7.0_10-b18)
    Java HotSpot(TM) 64-Bit Server VM (build 23.6-b04, mixed mode)Code language: CSS (css)

    これでjavaが実行できるようになりました。

    JAVA_HOMEを定義

    echo JAVA_HOME="/usr/local/jdk1.7.0_10/" >> /etc/environment
    JAVA_HOME="/usr/local/jdk1.7.0_10/"
    echo $JAVA_HOMECode language: PHP (php)

    を実行して、環境変数にJAVA_HOMEを定義します。

    実行

    cd atlassian-jira-5.2.4-standalone/
    bin/start-jira.sh

    これでもう実行できるようです。

    JIRAへアクセス

    http://localhost:8080/

    へアクセスします。

    なんかでてきた

    スクリーンショット 2013-01-03 7.29.43

    どうやらJIRAのホームディレクトリがないというエラーのようです。JIRAのホームディレクトリを指定するには環境変数としてJIRA_HOMEを定義してやればいいようです。

    echo JIRA_HOME="/home/nocturne-project/JIRA/" >> /etc/environment
    JIRA_HOME="/home/nocturne-project/JIRA/"
    echo $JIRA_HOMECode language: PHP (php)

    環境変数の設定が完了したら、一度JIRAのプロセスをキルします。

    ps aux | grep JIRA
    kill (JIRAのpid)

    あらためて実行

    スクリーンショット 2013-01-03 7.41.26

    おお!来ました!

    データベースの選択では、とりあえずお試しなので内部を選択しました。この操作には少し時間がかかります。

    アプリケーションの設定

    スクリーンショット 2013-01-03 7.43.33

    評価ライセンスの発行

    スクリーンショット 2013-01-03 7.45.51

    アンケートが求められる

    スクリーンショット 2013-01-03 7.46.42

    管理者アカウントの登録

    スクリーンショット 2013-01-03 7.48.08

    送信メールサーバーの設定

    スクリーンショット 2013-01-03 7.49.05

    ようこそ画面

    スクリーンショット 2013-01-03 7.49.57

    ダッシュボード

    System Dashboard - test

    プロジェクト管理画面

    Demonstration - test

    これには驚きました。メニュー部分がAJAXの非同期読み込みで、ぬるぬる画面が切り替わります。
    スクリーンショット 2013-01-03 7.52.57

    課題作成画面(チケット作成画面)

    スクリーンショット 2013-01-03 7.54.09
    なんとモーダルウィンドウ!

    スクリーンショット 2013-01-03 7.56.29

    スクリーンショット 2013-01-03 7.54.09
    課題(チケット)の作成は非同期通信で一瞬で終わる。早い!

    課題リスト(チケットリスト)

    スクリーンショット 2013-01-03 7.58.16
    見て分かる通り、検索結果のフィルタは保存できる機能がすごい!

    スクリーンショット 2013-01-03 7.59.33
    ネジマークをクリックすれば、各チケットにダイレクトに状態の変更が可能!

    課題のクローズ

    スクリーンショット 2013-01-03 8.00.33スクリーンショット 2013-01-03 8.00.42

    課題のクローズもほんの一瞬で終わる。

    一目瞭然なチャート

    スクリーンショット 2013-01-03 8.01.41
    まだあまり課題を登録していないのでわかりずらいですが、課題数ベースで作成した課題と完了済みの課題との比較チャートです。一目均衡表の雲のようです。

    誰の作業量が一番多いのか、少ないのかわかる課題画面

    スクリーンショット 2013-01-03 8.03.43
    この機能こわいですね。作業量ベースで差が丸見えというか・・。力を魅せつけられるというか・・。

    プラグインの導入

    スクリーンショット 2013-01-03 8.06.26

    gitプラグイン(有料)

    スクリーンショット 2013-01-03 8.09.27
    これはマイナスポイントかなぁ・・。デフォルトではCVSだけなのかなリポジトリ管理は。

    スクリーンショット 2013-01-03 8.12.02
    あれっ・・コミットしてたはずなのですが・・・。

    まとめ

    メリット

    • AJAXによる非同期読み込みで画面遷移をほとんど発生させずストレスのない挙動
    • 非プログラマな職の人でも使いやすい。直感的でわかりやすい
    • グラフが見やすい
    • 進捗管理しやすい
    • 他の人の作業状況が把握しやすい
    • シンプル
    • プラグインが簡単にいれられる
    • 最初のインストールが少しむずかしいが、インストールさえできれば簡単

    デメリット

    • リポジトリ管理がヒドイ
    • シンプルすぎて細かいところに手がとどいてない感じがする
    • バージョンやマイルストーン・・・どこ・・?
    • やっぱりバーンダウンチャートがほしい
    • RedMineのTime Trackerプラグインには勝てない
    • Redmineのチャートプラグインのほうが高機能

    追記(2013/07/20)

    JIRAのの最新版では下記の機能が追加されていました。かなり使えるようになっており、RedMineを超えているかもしれません。

    • マイルストーン(バージョン管理)
    • bitbacket連携によるgitの利用(5userまで無料)
    • TEMPOプラグインによるタイムトラッキング機能(1000円買いきりの機能)
  • Javascriptテストフレームワーク Jasmineを試す

    日頃からJavascriptで開発をしているのにも関わらずあまりテストを書かないので、ここは本格的にテストを書こうと調べてみました。JavascriptのテストフレームワークといったらJsUnitなのかなーと思っていたが、調べてみると結構いろんな種類のテストフレームワークがあったりして、その中で得に人気なのかどうやらJasmineらしい。

    Jasmine ~ JavaScript Test フレームワーク より引用:
    今回は, JavaScript のテストを行うためのフレームワークJasmine の紹介です。
    JavaScript のテストといえば, JSUnit が有名です。
    JSUnit は, JUnit とに似たような, Matcher が利用できたりしてわかりやすいのですが,
    開発やメンテナンスがストップしており, またWebプロジェクトに組み込まないと利用できないことが
    ちょっと残念です。

    JUnit のページでも紹介があるように, 今後は Jasmine というフレームワークを開発していくようです。

    なるほど、JsUnitはかつて人気だったテストフレームワークだったけども開発ストップしてて古くなっており、今はJasmineということなのか。

    とりあえずやってみた

    しかし、初めて触るライブラリというのはどうにもこうにも敷居が高い。少しずつ調べてみる。なんにせよまずはインストールだ。

    公式ページ: http://pivotal.github.com/jasmine/
    公式ページ(の翻訳): http://mitsuruog.github.com/jasmine/
    ダウンロード: https://github.com/pivotal/jasmine/downloads

    ダウンロードしたファイルは、jasmine-standalone-1.3.1.zip です。

    二種類の使い方

    Jasmineは使い方が二種類あるそうです。

    • standalone
    • rubygems + rake

    standaloneはRubyを使わずにJavascriptだけで使える環境、 rubygems + rake はテストを自動化させたい場合はこちらを選ぶ。ただ、standaloneでもPhantomJSを使えば自動化できるようです。

    ここではstandaloneについて実験

    rubygemsを使った方法だと、テストを実行させるためのHTMLを自動生成してくれて大変便利らしいのですが、ここでは導入としてstandaloneを使ったやりかたについて試してみたいとおもいます。

    ディレクトリの構造はこのようになっています。

    .
    ├── SpecRunner.html
    ├── lib
    │   └── jasmine-1.3.1
    │       ├── MIT.LICENSE
    │       ├── jasmine-html.js
    │       ├── jasmine.css
    │       └── jasmine.js
    ├── spec
    │   ├── PlayerSpec.js
    │   └── SpecHelper.js
    └── src
        ├── Player.js
        └── Song.jsCode language: CSS (css)

    これをWebサーバーにあげて、SpecRunner.html を開いてみます。

    スクリーンショット 2013-01-03 3.24.32

    なるほど、わからん。

    見た感じ、上の5つの丸がテストの成功か失敗かを表し、下段の黒字がテストスイート(カテゴリ分け)、緑の文字がテストケースという具合のようです。

    Specの意味

    「仕様」 = テストケース

    Suiteの意味

    「一式(分類)」 = テストスイート

    SpecRunner.htmlの中身を見る

    SpecRunner.htmlの中身を見てみます。

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
      "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
      <title>Jasmine Spec Runner</title>
    
      <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">
      <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
      <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
      <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>
    
      <!-- include source files here... -->
      <script type="text/javascript" src="src/Player.js"></script>
      <script type="text/javascript" src="src/Song.js"></script>
    
      <!-- include spec files here... -->
      <script type="text/javascript" src="spec/SpecHelper.js"></script>
      <script type="text/javascript" src="spec/PlayerSpec.js"></script>
    
      <script type="text/javascript">
        (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;
    
          window.onload = function() {
            if (currentWindowOnload) {
              currentWindowOnload();
            }
            execJasmine();
          };
    
          function execJasmine() {
            jasmineEnv.execute();
          }
    
        })();
      </script>
    
    </head>
    
    <body>
    </body>
    </html>Code language: HTML, XML (xml)

    よくわからなかったのでコメントを振ってみる

    よくわからなかったのでコメントを振ってみます。

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
      "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
      <title>Jasmine Spec Runner</title>
    
      <!-- ショートカットアイコン -->
      <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">
    
      <!-- スタイルシート -->
      <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
    
      <!-- Jasmineの読み込み -->
      <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
    
      <!-- Jasmineの読み込み -->
      <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>
    
      <!-- テストを行う対象ファイルを読み込み -->
      <script type="text/javascript" src="src/Player.js"></script>
      <script type="text/javascript" src="src/Song.js"></script>
    
      <!-- 独自定義のマッチャを読み込み -->
      <script type="text/javascript" src="spec/SpecHelper.js"></script>
    
      <!-- テストケースファイルを読み込み -->
      <script type="text/javascript" src="spec/PlayerSpec.js"></script>
    
      <script type="text/javascript">
        (function() {
    
          // Jasmineの環境設定オブジェクトを読み込み
          var jasmineEnv = jasmine.getEnv();
    
          // アップデートの間隔を1秒に設定?
          jasmineEnv.updateInterval = 1000;
    
          // テスト結果を取得するためのオブジェクトを取得
          var htmlReporter = new jasmine.HtmlReporter();
    
          // 不明・・・
          jasmineEnv.addReporter(htmlReporter);
    
          // 個別のテストスイートやテストケースをクリックすることで、テストスイートの一部分のみを実行して結果を取得する
          jasmineEnv.specFilter = function(spec) {
            return htmlReporter.specFilter(spec);
          };
    
          // ウィンドウをリロードするオブジェクト生成
          var currentWindowOnload = window.onload;
    
          window.onload = function() {
            if (currentWindowOnload) {
              currentWindowOnload();
            }
    
            // テスト実行
            execJasmine();
          };
    
          // テスト実行関数の定義
          function execJasmine() {
            jasmineEnv.execute();
          }
    
        })();
      </script>
    
    </head>
    
    <body>
    </body>
    </html>Code language: HTML, XML (xml)

    このファイルは、テストを行いたいJavascriptを読み込み、それに対してテストを定義し実行するというもののようです。しかし、「jasmineEnv.updateInterval = 1000;」という行が気になった。これはもしかして1秒ごとにテストを実行するということなのだろうか。もしそうなら、このHTMLファイルを開きっぱなしで常にテスト結果を確認できるということなのでとても嬉しいのだけれど・・。

    バグらせてみる

    まだなんだか感覚がつかめないので、テスト対象のファイルをバグらせてみることにします。

    Player.jsをバグらせる。

    function Player() {
    }
    Player.prototype.play = function(song) {
      this.currentlyPlayingSong = song;
      this.isPlaying = true;
    };
    
    Player.prototype.pause = function() {
      this.isPlaying = false;
    };
    
    Player.prototype.resume = function() {
      if (this.isPlaying) {
        throw new Error("song is already playing");
      }
    
      this.isPlaying = true;
    };
    
    Player.prototype.makeFavorite = function() {
      this.currentlyPlayingSong.persistFavoriteStatus(true);
    };Code language: JavaScript (javascript)

    中身はとてもシンプルで、本当にプレイヤー機能が入っているのかと思えばそんなことはなくて、模倣したオブジェクトのようです。

    コメントを振ってみます。

    function Player() {
    }
    
    // 再生する
    Player.prototype.play = function(song) {
      this.currentlyPlayingSong = song;
      this.isPlaying = true;
    };
    
    // ポーズする
    Player.prototype.pause = function() {
      this.isPlaying = false;
    };
    
    // 途中から再生する
    Player.prototype.resume = function() {
      if (this.isPlaying) {
        throw new Error("song is already playing");
      }
    
      this.isPlaying = true;
    };
    
    // お気に入りに登録
    Player.prototype.makeFavorite = function() {
      this.currentlyPlayingSong.persistFavoriteStatus(true);
    };Code language: JavaScript (javascript)

    お気に入りに登録のコメントが合っているのか不安ですが、これの途中から再生するをバグらせてみます。

    ...
    
    // 途中から再生する
    Player.prototype.resume = function() {
      if (this.isPlaying) {
        throw new Error("song is already ");
      }
    
      this.isPlaying = true;
    };
    
    ...Code language: JavaScript (javascript)

    「song is already playing」からplaying を消してみました。これでいったいなにが起きるか・・・。
    先ほどの SpecRunner.html を開いて確認してみます。

    バグがある場合のテスト結果

    スクリーンショット 2013-01-03 4.10.31

    おお! なんか出ました!エラーがでました!これはエラー詳細ページで、全体のテストケース一覧を見るには「5 specs」をクリックします。

    スクリーンショット 2013-01-03 4.11.31

    エラーがあるテストケースは赤字になっています。
    また、自動的にリロードしてくれるのか期待していたのですが、自動リロード機能はないようなので、ブラウザの自動リロード機能を使って常に監視しておくといいです。

    テストのほうを弄る

    テストケースが定義されているファイルをいじってみます。弄るファイルはspecフォルダにある「PlayerSpec.js」です。

    中身はこんなかんじ

    describe("Player", function() {
      var player;
      var song;
    
      beforeEach(function() {
        player = new Player();
        song = new Song();
      });
    
      it("should be able to play a Song", function() {
        player.play(song);
        expect(player.currentlyPlayingSong).toEqual(song);
    
        //demonstrates use of custom matcher
        expect(player).toBePlaying(song);
      });
    
      describe("when song has been paused", function() {
        beforeEach(function() {
          player.play(song);
          player.pause();
        });
    
        it("should indicate that the song is currently paused", function() {
          expect(player.isPlaying).toBeFalsy();
    
          // demonstrates use of 'not' with a custom matcher
          expect(player).not.toBePlaying(song);
        });
    
        it("should be possible to resume", function() {
          player.resume();
          expect(player.isPlaying).toBeTruthy();
          expect(player.currentlyPlayingSong).toEqual(song);
        });
      });
    
      // demonstrates use of spies to intercept and test method calls
      it("tells the current song if the user has made it a favorite", function() {
        spyOn(song, 'persistFavoriteStatus');
    
        player.play(song);
        player.makeFavorite();
    
        expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
      });
    
      //demonstrates use of expected exceptions
      describe("#resume", function() {
        it("should throw an exception if song is already playing", function() {
          player.play(song);
    
          expect(function() {
            player.resume();
          }).toThrow("song is already playing");
        });
      });
    });Code language: JavaScript (javascript)

    うおお..よくわからない…

    RSpecというruby界隈の方にはお馴染みの書き方のようです。そしてこの書き方がテストをやる上ですごいやりやすいんだとか。
    また同じようにコメントを振ってみます。

    // テストスイート定義
    describe("プレイヤー", function() {
    
      // テストを行うJavascriptのオブジェクトを読みこませる変数の宣言
      var player;
      var song;
    
      // テストを開始する準備を行う 
      beforeEach(function() {
        // テスト対象となるオブジェクトの読み込み
        player = new Player();
        song = new Song();
      });
    
      // テストケースを定義 it("テストケース名", 無名関数)
      it("曲を再生することができる", function() {
    
        // 音楽を再生
        player.play(song);
    
        // player.currentlyPlayingSong が song と同値であることを期待する
        expect(player.currentlyPlayingSong).toEqual(song);
    
        // プレイヤーが再生中であることを期待する -> SpecHelper.js に定義されています
        expect(player).toBePlaying(song);
      });
    
      // テストスイート定義(階層になっていて見やすい)
      describe("曲が一時停止されたときの挙動", function() {
    
        // テストを開始する準備を行う
        beforeEach(function() {
    
          // プレイヤーを再生
          player.play(song);
    
          // プレイヤーをポーズ
          player.pause();
        });
    
        // テストケース定義
        it("曲が一時停止しているか確認する", function() {
    
          // player.isPlaying が false になっている
          expect(player.isPlaying).toBeFalsy();
    
          // プレイヤーが再生されているマッチャに対しnotメソッドによって評価が逆になり、プレイヤーは音楽を再生していない となる
          expect(player).not.toBePlaying(song);
        });
    
        it("途中から再生可能かどうか", function() {
    
          // プレイヤーをレジュームする
          player.resume();
    
          // player.isPlaying は true になっている
          expect(player.isPlaying).toBeTruthy();
    
          // player.currentlyPlayingSong は song と 同じ値であることを期待する
          expect(player.currentlyPlayingSong).toEqual(song);
        });
      });
    
      // テストケース定義
      it("ユーザーがお気に入りにしていた場合、その曲を教える", function() {
    
        // オブジェクトのメソッドの呼び出しをスパイを使って監視
        spyOn(song, 'persistFavoriteStatus');
    
        // プレイヤーを再生
        player.play(song);
    
        // プレイヤーがお気に入りに設定(内部で this.currentlyPlayingSong.persistFavoriteStatus(true); が実行されます )
        player.makeFavorite();
    
        // song.persistFavoriteStatusメソッド は 引数に true を伴って実行されたか
        expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
      });
    
      // テストスイート定義
      describe("#レジューム", function() {
        it("曲が既に再生されている場合に例外をスルーする", function() {
    
          // プレイヤーを再生
          player.play(song);
    
          // プレイヤーをレジュームしてその例外が同じものであることを期待する -> player.resume(); では throw new Error("song is already"); として例外が送出されている。
          expect(function() {
    
            // レジューム
            player.resume();
    
          }).toThrow("song is already playing");
        });
      });
    });Code language: JavaScript (javascript)

    頭を使いすぎた・・・。だいたいこんなかんじで合っているとおもいます。
    流れはコメントの通りで、expect の期待が外れると、そのテストケースは「失敗」となるようです。describeによるテストスイートのカテゴリ分けや階層化もなかなかおもしろいと思いました。

    そして、とくに気になった点を2つあげると、独自定義マッチャと、スパイ機能です。これについては次に書きます。そしてその前に、マッチャってなんだ・・・・・。

    マッチャ

    マッチャとは、「AはBであることを期待する」というように正しいかどうかを評価するためのものです。
    マッチャはいくつかの種類があります。

    expect(x).toEqual(y); xとyが等しいことを期待する
    expect(x).toBe(y); xとyが同じオブジェクトであることを期待する
    expect(x).toMatch(pattern); 文字列または正規表現パターンでxと比較し、一致することを期待する
    expect(x).toBeDefined(); xがundefinedではない場合ことを期待する
    expect(x).toBeUndefined(); xがundefinedであることを期待する
    expect(x).toBeNull(); xがnullであることを期待する
    expect(x).toBeTruthy(); xがtrueであることを期待する
    expect(x).toBeFalsy(); xがfalseであることを期待する
    expect(x).toContain(y); 配列化か文字列であるxに対して、yが含まれていることを期待する
    expect(x).toBeLessThan(y); xがy未満であることを期待する
    expect(x).toBeGreaterThan(y); xがyよりも大きいことを期待する
    expect(function(){fn();}).toThrow(e); 無名関数が実行された時に関数fnが例外eを投げることを期待する
    .not.(matcher) (matcher)に他のマッチャを指定することでそのマッチャを逆に評価します(trueをfalseに falseをtrueに)

    独自マッチャの定義

    マッチャは予め定められたものだけではなく、「SpecHelper.js」によって独自に定義することもできます。

    // テストを実行する準備をする
    beforeEach(function() {
    
      // マッチャの追加
      this.addMatchers({
    
        //toBePlayingを追加
        toBePlaying: function(expectedSong) {
    
          // this.actual は expect(player).toBePlaying(song); の player の部分
          var player = this.actual;
    
          // 評価
          return player.currentlyPlayingSong === expectedSong && 
                 player.isPlaying;
        }
      });
    });
    Code language: JavaScript (javascript)

    スパイ

    次はスパイを見ていきます。

      // テストケース定義
      it("ユーザーがお気に入りにしていた場合、その曲を教える", function() {
    
        // オブジェクトのメソッドの呼び出しをスパイを使って監視
        spyOn(song, 'persistFavoriteStatus');
    
        // プレイヤーを再生
        player.play(song);
    
        // プレイヤーがお気に入りに設定(内部で this.currentlyPlayingSong.persistFavoriteStatus(true); が実行されます )
        player.makeFavorite();
    
        // song.persistFavoriteStatusメソッド は 引数に true を伴って実行されたか
        expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
      });Code language: JavaScript (javascript)

    スパイのテストケースはこうなっています。「spyOn(song, ‘persistFavoriteStatus’);」で song オブジェクトの persistFavoriteStatusメソッドを監視します。

    songのpersistFavoriteStatusメソッドはこのようになっています。

    function Song() {
    }
    
    Song.prototype.persistFavoriteStatus = function(value) {
      // something complicated
      throw new Error("not yet implemented");
    };Code language: JavaScript (javascript)

    「player.play(song);」でプレイヤーを再生するときには特に内部でスパイは活動を行いませんが、「player.makeFavorite();」で活動を行います。

    makeFavoriteメソッドは内部ではこのようになっており、「this.currentlyPlayingSong」には 「player.play(song);」内部で定義された songオブジェクトが入っているため、「persistFavoriteStatus」が実行できます。

    // お気に入りに登録
    Player.prototype.makeFavorite = function() {
      this.currentlyPlayingSong.persistFavoriteStatus(true);
    };Code language: JavaScript (javascript)

    このときに、スパイが活動を始め、このメソッドが実行されたことをしっかりと記憶します。

    そして、

    // song.persistFavoriteStatusメソッド は 引数に true を伴って実行されたか
        expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);Code language: JavaScript (javascript)

    スパイの活動を見るために、toHaveBeenCalledWithメソッドがスパイから songオブジェクトがpersistFavoriteStatusメソッドを実行したときに引数にtrueを使用したかを聞き出します。

    true が引数に使われたことがわかったので、これで期待通りの結果になっているということがわかります。

    スパイのためのマッチャ

    このように toHaveBeenCalledWith のようなスパイのためのマッチャがいくつか用意されています。

    expect(x).toHaveBeenCalled() xメソッドがスパイ中に呼び出されていたメソッドであることを期待
    expect(x).toHaveBeenCalledWith(arguments) xメソッドがスパイ中に呼び出された時にそのメソッドに使用していた引数がargumentsであることを期待
    expect(x).not.toHaveBeenCalled() xメソッドがスパイ中に呼び出されなかった
    expect(x).not.toHaveBeenCalledWith(arguments) xメソッドがスパイ中に呼び出された時にそのメソッドに使用していた引数がargumentsでないことを期待

    さまざまなスパイの呼び出し方法

    スパイが監視しているメソッドの実行を検知したときに細かい挙動を指定することができます。

    spyOn(x, ‘method’).andCallThrough(); デフォルト機能。スパイ活動を開始します。
    spyOn(x, ‘method’).andReturn(arguments); スパイが呼び出されたときに決められた引数であるargumentsを返します。
    spyOn(x, ‘method’).andThrow(exception); スパイが呼び出された時に渡された例外をスルーします。
    spyOn(x, ‘method’).andCallFake(function); スパイが呼び出された時に指定された関数へ実行を移譲します。

    最後に

    とても使いやすくて、ひとつのHTMLページに詰め込むこともできるし、デバッグモードをONにした場合にのみ表示なんてことも不可能ではないので、いろんな応用が効くテストフレームワークだと思いました。こんなに柔軟性のあるテストフレームワークだということに今更ながら驚いています。

    スクリーンショット 2013-01-03 5.57.54