$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

navigator.userAgentをモックしてJSのユニットテストをする

 JavaScriptユニットテストでnavigator.userAgentをモックしてテストしたいことがあり、そのようなユーティリティを作ってテストをしてみたのでメモ。

 ちなみにいろいろ試した例は https://github.com/shibayu36/javascript-playground/blob/master/es2015-project/test/stubUserAgent.js にあるので、参照してください。

やりたいこと

 最近はkarmaやkarma-html2js-preprocessorを利用することで、DOMに依存するテストでもHTMLの断片を用意してユニットテストを行うことができるようになった。TypeScript環境でそのようなテストをする方法については 以前の記事 で紹介した。またES2015版でのテスト環境を作った例は https://github.com/shibayu36/javascript-playground/tree/master/es2015-project あたりにおいている。

 DOMに依存するユニットテストを行っていると、navigator.userAgentに応じて処理を変えているような実装をテストしたいことがある。navigator.userAgentに応じて処理を変えている例としては、特定のブラウザで動かない機能のUIをそのブラウザでは隠すなどといったものがある。

 そのようなテストをしたい場合、テスト時のみ一時的にnavigator.userAgentを書き換え、そのテストが終わったら元に戻すということをして、挙動をユニットテストしたい。つまりnavigator.userAgentを一時的にモックしたい。

 利用のイメージとしては次のコードのとおり。stubUserAgent関数を呼ぶと一時的にnavigator.userAgentを書き換え、この関数から返ってきたオブジェクトのrestoreメソッドを呼ぶことで元に戻すということをしている。

it('ユーザーエージェントをモックしてテストする', () => {
    let defaultUserAgent = navigator.userAgent;

    // ここでnavigator.userAgentを「StubbedAgent/1.0」に書き換える
    let stub = stubUserAgent('StubbedAgent/1.0');

    // ここでuserAgentに応じて処理を変えるような実装をテストする
    // 現在は簡単のためnavigator.userAgentが変わっているかどうかだけ見ている
    assert.equal(navigator.userAgent, 'StubbedAgent/1.0');

    // テストが終わったらrestoreメソッドを呼ぶと元のuserAgentに戻る
    stub.restore();
    assert.equal(navigator.userAgent, defaultUserAgent);
});

stubUserAgentを実装する

 上で紹介したようなnavigator.userAgentを一時的にモックすることを実現するために、stubUserAgent関数を実装してみた。Chrome, Firefox, jsdomの環境下ではおそらく動くと思う。

function stubUserAgent(userAgent) {
    // もともとのPropertyDescriptorを保存しておく
    let origDescriptor = Object.getOwnPropertyDescriptor(
        navigator, 'userAgent'
    );

    // navigatorのuserAgentプロパティを
    // 渡されたuserAgentが返るように書き換える
    Object.defineProperty(navigator, 'userAgent', {
        get: function () { return userAgent },
        enumerable: true,
        configurable: true,
    });

    // restoreを呼べるようなオブジェクトを返す
    return {
        restore() {
            if (origDescriptor) {
                // origDescriptorがあるなら、definePropertyで戻す
                Object.defineProperty(navigator, 'userAgent', origDescriptor);
            }
            else {
                // origDescriptorがないなら、navigatorのprototypeで
                // userAgentが定義されているはず。それならば、モックで
                // 定義したuserAgentプロパティをdeleteすれば戻せる。
                delete navigator.userAgent;
            }
        },
    };
}

 手順としては、もともとの状態を取っておき、deinePropertyを使ってnavigator.userAgentを再定義し、restoreを呼んだ時に元に戻すというだけである。ただ、navigatorのオブジェクト自体にuserAgentプロパティがある場合と、prototypeの方でuserAgentが定義されている場合の二通りがあるため、少し複雑化している。

なぜdefinePropertyで定義しなければいけないか

 ちなみに単純に考えると以下のようにすればうまくいくのでは?と思えてしまう。しかし、この方法だとnavigator.userAgentへの代入が出来ないためにうまくいかない。

function stubUserAgent(userAgent) {
    let origUserAgent = navigator.userAgent;
    navigator.userAgent = userAgent;

    return {
        restore() {
            navigator.userAgent = origUserAgent;
        }
    };
}

 JSではプロパティにもいろんなメタ情報があって、そのメタ情報を設定することで以下のような項目などを制御できる。

  • getterやsetterとして定義する(get, set)
  • プロパティへの代入の可否(writable)
  • プロパティの再定義やdeleteの可否(configurable)
  • 列挙の可否(enumerable)

 この辺りについてはdefinePropertyのドキュメント が詳しい。

 そして、navigator.userAgentはgetterとして定義されていて、またwritableはfalseとして定義されているため、プロパティへの値の代入は出来ない状態になっている。しかし、configurableという属性はtrueとなっているため、definePropertyという関数を使えば再定義することは可能となっている。これらの理由から、一時的に書き換えければdefinePropertyを使うしか方法がなかった。

 もしconfigurableさえfalseになっていると、そもそも値の書き換えを行うことはできなくなってしまう。このため、navigator.userAgentプロパティのconfigurableをfalseとしているSafariにおいては、上のような実装すらも動かない。こうなってくると、userAgentを判定するようなクラスを作り、そのクラスのメソッドをモックするという方法を使うしかないだろう。

まとめ

 今回はnavigator.userAgentの値を一時的に書き換えてテストする方法について書いてみた。正直こういう方法で良いのか不安なので、よりクールな方法があれば教えてほしいです。

HTML::Lint::PluggableにPRした時に参考にした資料

 HTML::Lint::Pluggableという、HTMLが正しい形式かをチェックしてくれるモジュールがある。しかし、自分が使いたいHTML5のタグの中で許可していないものがいくつかあったのでPRして追加してもらった。今回はbdi, dialog, menuitem, template, track, rb, rtc辺りを許可してもらった。

github.com

 PRするにあたって、いくつか参考にした資料があったのでメモしておく。

 許可するタグをPRしようと考えた時に、まず実際に自分が追加したいタグがHTML5として定義されているタグなのかを調べないといけないと考えた。そこで以下の資料を参考にした。

 1つ目はwhatwgが定義しているHTML Living Standardの資料で、このElementsの部分に書かれているのがタグ一覧のようだった。2つ目はW3Cが定義しているHTML5の資料で、こちらもElementsに書かれているのがタグ一覧のようだった。比べてみると微妙に違いがあるので、少なくともどちらか一方に載っているElementsであれば追加しても良さそうと判断した。

 これを調べていると、なぜW3Cwhatwgが両方共HTMLの仕様を定義し、かつ微妙に違うのかという疑問が生じた。これは今追いかけ中なのだけど、以下の資料が参考になりそうだった。

YAPC::Asia Hachioji 2016に行きました #yapc8oji

 7/2〜7/3にあったYAPC::Asia Hachioujiに行きました。二日間とも朝から参加したのですが、本当に面白い発表が多く、また東京にいるいろんな人と議論とかができたので、非常にためになりました。

 特に東京周りの人と議論できたのが良かったです。いつもは京都にいて、自分や会社の人といろいろ考えながらサービスを作ったり、チームビルディングしたり、またエンジニアリングの設計をしたりなどといったことをやっているのですが、このような機会に他の会社の人と議論をすることにより、自分のやっていることの方向性が良いのかなどを判断できました。朝早く京都から参加してよかった!

 ちなみに僕は以下の3つの発表が非常に印象に残りました。

  • つくって学ぶLinuxコンテナの裏側
  • 2人で楽しくサービスやアプリを作る話
  • あの日見たM-V-WhateverのModelを僕たちはまだ知らない

つくって学ぶLinuxコンテナの裏側

https://speakerdeck.com/hayajo/tukututexue-bulinuxkontenafalseli-ce

 hayajoさんの「つくって学ぶLinuxコンテナの裏側」が面白かったです。

 最近はDockerとかが流行ってきて、コンテナ技術がいろんな人に使われるようになっていますが、その基礎技術を学ぶために自分でminimalな実装を段階的に実装していくという話でした。僕はDockerは多少触ったことがあるのですが、その下にどのような技術があるかについてはまだ調べたことがありませんでした。しかし前提知識があまりなくても、発表を聞くだけで概要について学べました。

 ちなみにその後直接会って話していたのですが、自作コンテナを作るときにはセキュリティで考慮する点がいろいろあるので、自作で作るならその辺りをちゃんと知っておかないといけないし、まあ実際に使うなら自作じゃないほうがいいのではみたいな話をしてました。まあWebアプリケーションフレームワークに既存のものを使うか、自分で自作するかという話と近しいところがあるなあと思いました。

2人で楽しくサービスやアプリを作る話

http://www.slideshare.net/kamadango/2-63676859

 kamadangoさんの「2人で楽しくサービスやアプリを作る話」も印象に残っています。

 個人的にはこの発表の中の、「ものづくりのフロー」という、ものづくりのために、哲学・ビジョン・コンセプト・デザイン・プロトタイピングということを考えてやっていくという部分が面白かったです。

 この発表を聞いた時に、「こういうサービスがあったらいいかも」というアイデアは、この発表の中の「コンセプト」という部分に対応しているのではと思いました。けれどアイデアを思いついたとしても、ではそのアイデアの背景にある「哲学」や「ビジョン」をちゃんと考えておくことによって、デザインや実装をしている時に軸がブレずにサービスを作れるだろうなあと思いました。

 この「ものづくりのフロー」というのは、企画を考える上での一つのフレームワークだと思ったので、一度試してみようかと思いました。

あの日見たM-V-WhateverのModelを僕たちはまだ知らない

https://speakerdeck.com/shinpeim/afalseri-jian-ta-m-v-whateverfalse-modelwopu-tatiha-madazhi-ranai

 shinpeiさんの「あの日見たM-V-WhateverのModelを僕たちはまだ知らない」も非常に面白かったです。個人的には今回一番面白かったです。

 MVPとかMVVMとかMVなんとか系がなぜ必要になったのかとか、Presentation Domain Separationとはなにかとか、さらに本題として、ではその上でModelはどのように設計していくと良いのかなどについて発表されていて、非常に勉強になりました。連打マシーンの例も非常に分かりやすく、また最後のほうで理想と現実についても軽く話していて参考になりました。

 ちなみに前日の懇親会と、発表後にちょっと議論をさせてもらって、自分のやっていることの方向性は間違っていないのかとかを確認させてもらいました。特に、CQRSのコマンドの部分はしっかりLayered Architectureでやるけど、クエリの方はLayeredまでにしていないこともある、みたいな話をしていたのが面白かったです。あとFluxと他のパターンの関係とかも議論したかったのですが、それは時間切れで話せませんでした。また東京に行って議論したい。

 MVPとかMVVMとかFluxとかの話を追いかけるのも良いですが、こんな感じでさらにその背景を学ぶのも重要ですね。

最後に

 今回発表も面白かったですが、やっぱり運営していただいた皆さんに一番感謝しています。

 ちなみに自分も簡単な勉強会をしていたこともあるのですが、その小さい規模の勉強会の運営も非常に大変でした。今回のカンファレンスは外から見ると簡単そうに運営していたように見えますが、250人規模で、非常にためになるもので、しかも特にトラブルが無く運営されているというのはものすごいことだなと思いました。

 今回のカンファレンス、非常に楽しませてもらいました。運営の皆さんお疲れ様でした!