株式会社クリアコード > ククログ

ククログ

タグ:

自己流JavaScriptを書いていた人がAngularJSのユニットテストで躓いた点

結城です。

最近、AngularJSを使ったWebアプリ開発のプロジェクトに参加する事になり、とりあえず一通りの事は把握しておかなければと思って公式のチュートリアル(英語)を実践してみたのですが、JavaScriptの経験が浅い人だとハマらなさそうだけれども、中途半端に経験があったせいでドハマり、という場面に遭遇してしまいました。 恥ずかしい話ですが、せっかくなので同じように躓いている人(もしいれば)のために、分かった事や理解のポイントを書き記しておこうと思います。

この記事の対象読者は、以下のような状況にある人です。

  • フレームワークを使わないJavaScript(例えば、jQueryを使ったJavaScript程度)は書いた事がある。
  • 自動テスト(特に、ユニットテスト)は書いた事がある。
  • AngularJSを始めたばかりである。
  • 依存性注入という概念は理解できるが、実際にどう使うかはあまり知らない。

依存性注入って何?

ソフトウェアの設計の話で、依存性注入(dependency injection、DI)という考え方があります。

例えば、Webアプリの開発においてサーバー(が提供するWeb API)との通信を行う場面はよくありますが、JavaScriptの世界で最も低レイヤのやり方として、XMLHttpRequestを使う方法があります。 XMLHttpRequestを使ってWeb APIと通信する事が前提のWebアプリは、「XMLHttpRequestというクラス」であったり「接続先のWeb API」であったりといった要素に依存していると言うことができます。

このようなWebアプリで問題になるのは、自動テストのしにくさです。 WebサーバやAPIサーバを用意して実際にWeb APIを提供するのは骨が折れますし、テストの実行ごとにまっさらな環境を整え直すというのも難しいです。

そこで登場するのが依存性注入、略してDIという考え方です。

例えば上記のようなWebアプリでは、「接続先のWeb APIのURL」を設定で変えられるようにしておけば、本物のWeb APIを用意しなくてもダミーのAPIサーバで動作テストが可能です。

また、XMLHttpRequestを使う処理を共通化してhttp()のような名前のユーティリティ関数として括り出しておき、Web APIとの通信は全てこの関数を使うようにしておけば、

  • 本番環境では、実際にWeb APIとの通信を行う。
  • 動作テストの時は、http()というユーティリティ関数の定義を置き換えて、実際の通信を行わずに、あらかじめ与えられた固定のJSON文字列を、あたかも実際の通信結果がそれであったかのような形で返すようにする。

という具合で、http()の置き換えだけで自動テストを簡単に行えるようになります。

このように、「本体の処理が依存している外部との接続箇所を変更可能にしておいて、本番環境とテストの時のように場面に応じて実装を切り替える」という設計のパターンをDIと言います。 DIという言葉は知らなくても、そういう設計のコードを書いた事があるという人は少なくないでしょう。

AngularJSでのDI

上記チュートリアルのステップ5ではXMLHttpRequestを使った通信と依存性注入の事について触れられています。

AngularJSでは、XMLHttpRequestをそのまま使うのではなく、AngularJSが提供している$httpというモジュール(正確には、このモジュールが内部で使用している、$httpBackendという別のモジュール)の機能を通じてWeb APIと通信する事が強く推奨されています。 同じ$httpという名前のモジュールであっても、実際に機能を使う時にはAngularJSというフレームワークによって

  • 本番運用では、実際のWeb APIと通信する。
  • 自動テストでは、あらかじめ用意しておいたダミーのレスポンスを使う。

という具合に挙動が変化する(自動的に実装が切り替わる)ため、自動テストをしやすくなっています。 「実際に通信する処理の切り替えはどうやってやるのか?」は、「フレームワークのユーザー」であるWebアプリ開発者は気にする必要はなく、単に$httpというモジュールを使うことを宣言するだけでOKです。

その宣言の仕方として、AngularJSではinject(function(モジュール1, モジュール2, ...) { 実行したい処理 })という書き方をすることになっています。 このように書くと、AngularJSはinject()に渡された関数の引数に書かれているモジュール1モジュール2といった名前のモジュールを探してきて、それらを実際に引数として渡して、その関数を実行します。

もっと平たく言うと、inject(function(モジュール1, モジュール2, ...) { 実行したい処理 })という書き方をすれば、「実行したい処理」の中でモジュール1モジュール2を利用できるようになります。 これが、AngularJSでのDIの基本です。

既存のフレームワークを使わずにDI的な事をやろうとすると、「どうやって依存モジュールを渡すのか」や「どうやって実装を切り替えるのか」といった点が悩み所になりますが、AngularJSでは「使いたいモジュールは関数の引数に書いておけばそれでいい。モジュールの実装は、フレームワークがその引数名に対応する物を勝手に探してくる。」という割り切りをすることで、Webアプリ開発者は極力何も考えなくていいようになっているわけです。よく考えられていますね。

AngularJSのユニットテストでのDI

このinject()を使ったDIの仕組みはAngularJS全体で共通して利用できるようになっていて、自動テストも例外ではありません。 例えば、上記チュートリアルのステップ5でも、ユニットテストのbeforeEach()(前処理の定義)を以下のように記述しています。

describe('PhoneListCtrl', function(){
  ...
  beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
    $httpBackend = _$httpBackend_;
    $httpBackend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);

    scope = $rootScope.$new();
    ctrl = $controller('PhoneListCtrl', {$scope: scope});
  }));
...

……という所まで読み進めた時点で、自分は躓いてしまいました。 _$httpBackend_って何なんだ!? と。

実のところ、自分はその時点では「AngularJSでのDI」の全貌をまだきちんとは理解できておらず、かろうじて、最初に述べた「DIの一般的な話」だけを理解できていました。 その時の認識で上記のコード片を見ると、なんとなく、以下のように読めてしまっていました。

  • 本番用の$httpBackendの実装と、テスト時用の$httpBackendの実装を切り替える必要がある。
  • その切り替えは、_$httpBackend_という書き方で行う。前後にアンダースコアが付いた名前で参照すると、テスト時用のダミー実装が渡される。

ところが、どれだけ探してもこのアンダースコア付きの名前での参照についての詳しい話が見つかりません。 そのため、このコードが意味している所を理解できず、チュートリアルの次のステップに進めずにいました。 (最初はとりあえず読み飛ばして次に進んでみたのですが、後でまたこの書き方が出てきたので、頻出する書き方なのであれば理解が怪しいまま進むのは危険だと感じて、そこで止まってしまいました。)

「同じ動作をする、別の書き方」で理解する

理解の糸口となったのは、実際に動くコードが手元にあった事でした。

AngularJSのチュートリアルでは、そのステップで動かす事になっているコードそのもののリポジトリ(各stepがタグになっていて、タグをチェックアウトすればそのstepでのコード全体を見られる)が公開されていて、手元にそれをcloneして実際に動かしながらAngularJSを理解できるようになっています。 そのリポジトリでstep5の内容を見てみると、以下のようになっていました。

describe('PhoneListCtrl', function(){
  ...
  beforeEach(module('phonecatApp'));
  beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
    $httpBackend = _$httpBackend_;
    $httpBackend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr', function() {
    ...
    $httpBackend.flush();
    ...
  });
  ...

これを見てまず疑問に思ったのが、「inject()は必ず使わないといけないものじゃなかったのか?」という事でした。 というのも、チュートリアルで出てきたそれまでのコードでは基本的にinject()を使っていたので、使っているコードと使っていないコードが混在しうるというのがまず驚きでした。

次に思ったのは、「これを必ずinject()を使うようにして書く事もできるのだろうか?」という事でした。 それでコードを書き換えながら実行していたところ、以下のように書き変えても結果は変わらないという事が分かりました。

describe('PhoneListCtrl', function(){
  ...
  beforeEach(inject(function($httpBackend, $rootScope, $controller) {
    $httpBackend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr',
     inject(function($httpBackend) {
    ...
    $httpBackend.flush();
    ...
  }));
  ...

また、以下のように書き換えても同様でした。

describe('PhoneListCtrl', function(){
  var backend;
  ...
  beforeEach(inject(function($httpBackend, $rootScope, $controller) {
    backend = $httpBackend;
    backend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr', function() {
    ...
    backend.flush();
    ...
  });
  ...

これを踏まえて最初のコードのコメントを改めて読み直してみた所、ようやく意味が分かりました。

  // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
  // This allows us to inject a service but then attach it to a variable
  // with the same name as the service in order to avoid a name conflict.

このコメントでは「injectorはモジュール名の前後に付けられたアンダースコアを無視する」と書かれていますが、自分はこれを深読みしすぎて、実装の切り替えなどの話と絡めて考えてしまっていました。 そうではなく、これは本当に文字通りに、「_$httpBackend_と書いても、$httpBackendと書いたのと同じに扱われる」という意味なのでした。

つまり、こういう事です。 一切の省略をしないでDIを素直に使って自動テストを書くと、

describe('PhoneListCtrl', function(){
  ...
  beforeEach(inject(function($httpBackend, $rootScope, $controller) {
    $httpBackend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr',
     inject(function($httpBackend) {
    ...
    $httpBackend.flush();
    ...
  }));
  ...

という風に、$httpBackendというモジュールをinject()する書き方を何度も何度も書かないといけません。

しかし、テストの数が増えてくると、何度も何度もinject()するのはいかにも冗長です。 そういう時に、フレームワークをあまり使わない・使った事がない人が思いつくのが、beforeEach()で一時的な変数に1回だけ代入するというやり方でしょう。

describe('PhoneListCtrl', function(){
  var backend;
  ...
  beforeEach(inject(function($httpBackend, $rootScope, $controller) {
    backend = $httpBackend;
    backend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr', function() {
    ...
    backend.flush();
    ...
  });
  ...

AngularJSのチュートリアルに含まれているコードに書かれている内容も実質的にはこれと同じなのですが、こちらのやり方には1つ問題があります。 それは、依存しているモジュールをどの名前で参照すればいいのかが分かりにくくなるという事です。 この例であればbackendという変数名を使っていますが、人によってはHTTP Backendの略でhbと名付けるかも知れませんし、あるいはアンダースコアを付けて_$httpBackendとしたり、ダラーを重ねて$$httpBackendとしたりするかも知れません。 1つの開発プロジェクトの中で、その時の気分や担当者によって同じ物をどう書くかの書き方がばらけてしまうと、複数人での共同作業や長期的なメンテナンスの際に、混乱の元になってしまいます。

これと似たような事象が、低レベルのJavaScriptを書く場面でもよくあります。 それは、thisにどのような別名を割り当てるかという場面です。

JavaScriptでは、thisが指す対象は関数の実行時の文脈によって変動するにも関わらず、コールバック関数という形で処理の一部を分けて書く場面が多発します。 そのため、Rubyなどの他の言語の感覚でコードを書くとエラーになる事があります。 例えば以下のようなコードです。

var oldValue = this.currentValue;
setTimeout(function() {
  var newValue = this.currentValue;
  // ↑ここでは this === undefined になっているのでエラーになる
  console.log('delta: ' + (newValue - oldValue));
}, 1000);

Function.prototype.bind()を利用できる状況では、以下のように書けば問題ありません。

var oldValue = this.currentValue;
setTimeout((function() {
  var newValue = this.currentValue;
  // ↑束縛されたthisなので、これは1行目のthisと同じ物。
  console.log('delta: ' + (newValue - oldValue));
}).bind(this) /* ←ここでthisを束縛 */, 1000);

Function.prototype.bind()を使えない場合は、以下のように書くのが一般的です。

var oldValue = this.currentValue;
var self = this; // ←ここで別名を割り当てている
setTimeout(function() {
  var newValue = self.currentValue;
  // ↑別名で参照しているだけで、これは1行目のthisと同じ物。
  console.log('delta: ' + (newValue - oldValue));
}, 1000);

この例ではselfという変数名でthisの内容を参照できるようにしていますが、この時使う変数名はthat_thisなど様々な流儀があります(自分はselfを使う事が多いです)。 そのため、他の人の書いたコードや他のプロジェクトのコードで違う流儀の物を見ると、コードが意味する内容を読み取るのに時間がかかったり、意味を誤解してしまったりする事があります。

AngularJSでは、この発想を逆転することで、このような混乱した状況の発生を予防していると言えます。 つまり、最初から、$httpBackendという名前のモジュールを_$httpBackend_という別名でも参照できるようにしてあるため、$httpBackendという本来のモジュール名を「複数の関数で参照するための一時的な別名」として使えるのです。 この「最初から使える別名」のおかげで、AngularJSでは「一時的な別名をどう決めればいいか?」という事に無駄に悩まなくてもいいようになっているわけです。

まとめ

自分はJavaScriptを使うプロダクトをフレームワークを使わずに開発していた時期が長かったため、上記の「thisの別名問題」のようなやり方がすっかり身に染みついてしまっていました。 そのため、同じ物を敢えて別の名前で参照する(できるようにしてある)という発想が無く、思わぬ所で理解に躓いてしまいました。 自分自身が知らず知らずのうちに固定観念に囚われてしまっていたことを意識させられる出来事でした。

また、「Webアプリケーションフレームワーク」と同じようにメタ的な存在である「テスティングフレームワーク」を開発する中で悩み所になりがちだった問題について、AngularJSでは利用者目線での使い勝手を優先して巧みな解決策を用意している事が分かりました。 自分が今後フレームワークを開発する際にも、「フレームワークの実装のしやすさ」だけに囚われず、フレームワーク利用者の利便性を最大化するための努力を惜しまないように気を付けたい、という思いを新たにした次第です。

オチは特にありません。

つづき: 2016-01-06
タグ: JavaScript
2015-07-10

クリアコードの公開リポジトリ

すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したプログラムが入ったSubversionリポジトリリポジトリの更新状況のRSS)の公開を始めました。

クリアコードでは既存のフリーソフトウェアの開発に参加したり、新しくmilter managerなどのフリーソフトウェアを開発したりしていますが、それらの開発成果の公開場所はケースバイケースとなっています。

  • 既存のフリーソフトウェアの開発に参加する場合は、基本的に、開発成果はアップストリームに還元しています。
  • 新しくフリーソフトウェアを開発する場合は、基本的には関連コミュニティで標準的なホスティングサイトを利用しています。例えば、Ruby関連のソフトウェアであればRubyForge、GNOME関連のソフトウェアであればgnome.orgといった具合です。
  • 関連するソフトウェアがGitHubGoogle Codeを利用している場合は、それらのサイトを利用することもあります。
  • 特に標準的なホスティングサイトが無い場合はSourceForgeを利用しています。

このように、クリアコードの開発成果のソースコードは様々なホスティングサイトのリポジトリにて管理、および公開されています。

まもなくクリアコードは設立から3年が経とうとしていますが、その間、プロジェクトを作るまでもないような小規模なソースコードがいくつかたまってきました。この度、そのようなソースコードをSubversionリポジトリで公開することにしました。

このリポジトリには現在、ページの一部を折りたたむfolding.jsや、このククログを生成するためのtDiary関連のスクリプト(日記のデータをSubversionで管理するIOバックエンド日記を静的なHTMLに変換するスクリプトなどの記事で述べた物)、Thunderbird用の各種アドオンのソースコードが入っています。誰でも自由にチェックアウトできますので、注意事項をご了承の上でどうぞご利用ください。

以下、現在入っているプログラムを簡単に紹介します。

注意事項

  • これらのプログラムはすべて無保証です。
  • プログラムは予告なく追加・削除・変更されることがあります。

JavaScript関連

tDiary関連

ククログだけではなく、milter managerのブログでも使っています。使い方はmilter managerのtdiary.confが参考になると思います。

  • /tdiary/subversionio.rb: tDiaryのSubversionバックエンド
  • /tdiary/gitio.rb: ↑のSubverionバックエンドのgitバージョンです。
  • /tdiary/html-archiver.rb: tDiaryのデータをHTML化するで紹介した物ですが、その後、カテゴリに対応するなどいくつかの点で改良されています。
  • /tdiary/patches/customizable-style-path.rb: スタイルファイルのパスをカスタマイズできるようにします。RDスタイルなど、標準では有効になっていないスタイルを使うときに便利です。本家に提案したのですが、rejectされたのでここに入っています。
  • /tdiary/plugin/classed-category-list.rb: class付きでカテゴリリストを生成します。ククログのトップに並んでいる「タグ: ...」の部分です。
  • /tdiary/plugin/date-to-tag.rb: 日付を本文の下に生成します。今思えば名前が悪いですね。後で変えるかもしれません。
  • /tdiary/plugin/link-subtitle.rb: サブタイトルをその記事のリンクにします。通常は日付がリンクになるのですが、このククログでは日付は本文の下に置いてあるので、代わりにサブタイトルをリンクにしています。
  • /tdiary/plugin/multi-icon.rb: ページアイコンとして、favicon.ico(ICO形式の画像)とfavicon.png(PNG形式の画像)を両方指定できるようにします。
  • /tdiary/plugin/title-navi-label.rb: 「前の日記」「次の日記」リンクのラベルをリンク先の日記のタイトルにします。
  • /tdiary/plugin/zz-permalink-without-section-id.rb: section_footerプラグインが生成するpermalinkから「#pXX」を削除します。ククログでは、1記事を1セクションにして同じ日には複数の記事を書かないという方針で運営しており、セクションIDが必要ないため、このプラグインを作成しました。ファイル名の「zz-」は、このプラグインの読み込み順序を最後の方にさせるためのものです。

Thunderbird関連

Thunderbird Add-ons - クリアコードで公開している、Thunderbirdのバグを回避するパッチや挙動の変更を行う拡張機能です。公開ページにも書いてありますが、これらの拡張機能は無保証です。業務上の必要性からの導入をお考えの場合は、Mozilla Firefox & Mozilla Thunderbird保守・サポートサービスのご利用もご検討ください。

おまけ

リポジトリの更新状況を配信しているRSSは、Subversionに標準添付のcommit-email.rbで生成しています。今回、RSSのタイトルや説明を日本語にしたかったので、Subversionのtrunkに--rss-titleと--rss-descriptionオプションを追加しました。また、--repository-uriで指定されたリポジトリのURIをコミットメールのX-SVN-Repositoryヘッダに設定するようにもしています。

Subversionリポジトリの整形表示にはRepos Styleを利用しています。mod_dav_svnにはSVNIndexXSLTというオプションがあって、それを利用しています。

タグ: Ruby | JavaScript | Mozilla
2009-04-06

ページの内容を折りたたむスクリプトを公開しました

UxUのページの過去のバージョンの詳細情報など、そのまま表示すると長くなってしまう内容を折りたたむめの簡単なスクリプトを書いてみました。せっかくなので、AGPLv3にて公開することにします。

このスクリプトを読み込んだ後で以下のようにすると、XPath式で示された要素が折りたたみ可能となります。複数のインスタンスを生成すれば、異なる種類の折りたたみ項目を作ることもできます。

1
2
3
4
5
new Folding(
  '/descendant::*[@class="items-should-be-folded"]',
  '詳細を表示',
  '詳細を隠す'
);

また、折りたたまれた要素をボタンのクリックで表示すると、URIの末尾にその情報が付け加えられた状態となりますので、その状態のページをブックマークしたり、特定の項目を表示した状態のページへリンクしたりすることもできます。

DOM3 XPathを使用しているため、IE6などのレガシーなWebブラウザ向けにはJavaScript-XPathを併用する必要があることに注意してください。

この手のスクリプトはアニメーション効果が派手な物などすでに色々あると思いますが、シンプルな物が好みな場合や、社内Wikiのようにあまり飾り気が無くても良い場面などに使ってみると良いのではないでしょうか。また、折りたたみの対象となる要素をXPath式で自由に指定できるので、すでにあるページの内容を変更せずに簡単に導入できるという利点もあります。

AGPL

ところで、皆さんはAGPLというライセンスについてはご存じでしょうか? 

AGPL(Affero GPL)はGPLの派生ライセンスの1つで、Webサービス用のプログラムで使われることを想定しています。このライセンスが適用されたプログラムのコードを利用したWebサービスは、そのWebサービスのユーザに対して、サービスを構成するすべてのプログラムのソースコードを公開する義務があります。それ以外の点は通常のGPLと同一です。

誤解している方もおられるかもしれませんが、AGPLやGPLでライセンスされたコードを利用すると、すぐにソースコードを公開する義務があるということはありません。AGPLやGPLは、プログラムのユーザ(AGPLの場合はそのサービスのユーザも含む)に対してはソースコード開示の義務がありますが、それ以外の関係ない人にまでソースコードを公開する義務はありません。例えば社内Wikiに上のスクリプトを組み込んだ場合であれば、社内の人にはソースコードを公開しなくてはいけませんが、社外にまで公開する必要はありません。「よく分からないけどGPLこわい!」と尻込みしてしまわずに、色々なコードを用途に応じて使い分けてみると良いでしょう。

つづき: 2009-04-06
タグ: JavaScript
2009-04-03

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
タグ: