自己流JavaScriptを書いていた人がAngularJSのユニットテストで躓いた点 - 2015-07-10 - ククログ

ククログ

株式会社クリアコード > ククログ > 自己流JavaScriptを書いていた人がAngularJSのユニットテストで躓いた点

自己流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では利用者目線での使い勝手を優先して巧みな解決策を用意している事が分かりました。 自分が今後フレームワークを開発する際にも、「フレームワークの実装のしやすさ」だけに囚われず、フレームワーク利用者の利便性を最大化するための努力を惜しまないように気を付けたい、という思いを新たにした次第です。

オチは特にありません。