なぜクライアントJavaScriptの単体テストを書くのが難しいか、考えてみた

ってsinonのスタブ漏れを探しながら何度目かわからない感じにキレてた。

とにかく仕事でJSのテスト書くのが辛いので考えてみる。比較的JSのテストに慣れてる自分ですら辛いのだから、世界はもっと辛いに間違いない。サーバーサイドのnode.jsの話ではない。

JavaScriptで完結しない

構造がHTMLの構造と密結合している。装飾や位置、表示/非表示はCSSによって制御されている。 クライアントJSはHTMLと密結合しており、CSSからビューは影響を受ける。それらがネットワークの結果を受け非同期に振る舞いを帰る。その最終的な値を取得するのが難しい。 もちろんサーバーサイドだってDBやネットワークという外部リソースを扱うが、モックの手法が確立しているし、局所的な複雑度は、JSの方がはるかに多い。

言語仕様が貧弱

mochaやjsmineはrspecを真似てるけど、本質的にJavaScriptの言語仕様にひきずられる為、表現に限界がある。JSはメタプロとの相性は正直微妙な部分がある。 undefined is not function

サーバーとクライアントのメンタルモデルの違い

1画面は1ユーザーに紐づく。サーバーは多数のユーザを扱う。 サーバーサイドのテスト書いてた人がクライアントでも同じようにやりゃいいじゃんって言ってくることがあるけど、そもそも興味の対象が違うので、同じように書けないことが多い。

AndroidiOSの設計も同じだと思うんだけど、クライアントコードは大量にシングルトンが必要になる。シングルトンが必要とされるのは一意なユーザと強烈に紐付いてるからである。ここのモジュラリティを高める必要はない。

こういう依存性を扱うには、うまくDIする仕組みが必要な気がしている。サーバーサイドのDIより実装は簡単だし需要が高い。

ってことで知識の転用が難しい。

本質的にグローバル依存

外部からそのプロパティが書き換えられてないことを保証する手段が貧弱。 DOMは巨大なグローバル変数だということもできて、これに対する操作を避ける事ができない。でもそもそもユーザーという存在がシングルトンだし、一人のユーザーに発生するUXも基本的には一意だ。このような中体験をもたらすモジュールを切り出して再利用することが難しい。

Selenium/Capybara等のE2Eテストが(あんまり)非同期を考慮してない

ボタンを押したら0.5秒間のポップアップ開閉アニメーションを挟んでDOM展開。みたいなコードを書くと、ボタンを押した時点で、1秒待って、次のボタンを押す、みたいなテストになるとする。このとき、画面が展開されたことを確認する方法がない。0.5秒じゃなくて1.2秒になったらテストが落ちる。邪道として毎秒10回ぐらいjQueryでクエリ投げればいいけど、DOMに対するクリックイベントが定義されてる保証がない。ビューは展開されたけど非同期にネットワーク経由でプロパティ取りにいってて、コールバック定義はその後かもしれない。

ってことを考えてると簡単にハマる。アクション単位の終了検知が異常に難しい。これはGUI開発環境全般に言える。Capybaraなんぞでこの表現できるか糞がって気分になる。

余談だけど、MVVMではビューにどんな値が展開された興味を持たずに、自分自身のプロパティに操作を集中させることで、この問題を解決しようとする。解決できるかはおいといて。VM -> Vの展開はライブラリの(データバインディングの)責務なので、テスト対象としては興味を持たない方が良い。

そもそも非同期が難しい

慣れろ。jQuery.DeferredかPromise/A使え。

JavaScriptのテストを書く文化が(最近まで)なかった

ネット上のユースケース資源、もといコピペ資源がない。毎回すごく考えてテストケースを作る必要がある。

フレームワークがテストを考慮してない

コンポーネントの初期化が独立しておらず他のコンポーネントに依存していると、テスト環境の構築が異常に重くなっていく。また、コンポーネントAPIドメインモデルがテストを考慮せず外部から振る舞いが観測できなかったりする。これはUnityやってるときもそうだった気がするからGUI全般の問題で、これがゲーム業界の人たち、テストを全く書かなかったりするけど、そもそも書けなかったする問題もある。テスト構築コストが重いから無視されていく。だからといってテストを書こうとする意思をみせようとすらしないのは良くないと思う。

持論

UXはユニットテストできない。ユーザーテストによるA/BテストのKPIや、ある種のアーティスティックな解決アプローチを取るしか無い。 感性に訴える要素のパラメータはプラガブルに調整できるべきだが、その値をテスト対象に含めてはいけないと思う。値を変えながら何度もテストするわけだから。

テストを書くコツは、とにかく外部依存を減らして独立したコンポーネント単位に切り出す。jQueryだったら this.$foo = $('.foo-container')みたいにDOMを切り出して、以降はthis.$fooにのみ操作を集中する。大域のクエリを投げない。これはモジュール性を破壊する。

それでも僕がテストを書くのは、モジュールの振る舞いを守り、テストを書こうというモチベーションが結果として良いコードを導くことを信じているからであり、テストを書かないという愚かな決定に対する抵抗であり、テストを書こうという意志をなくした瞬間そこに地獄が顕現する。動けばよい、動いているコードを触れるな、それを妥協するのが現実でありビジネスだ、と自分の怠惰を他人に押し付けて説教する連中への憎悪によってこの記事を提供している。自分の同世代のウェブエンジニアは上司が昔書いたコードをメンテしながら発狂してるんだよお前らの負債を俺らに受けつけてこちらはモチベーションをなくし仕事の評定がさがり上司は動くコードを提供したことで出世していやーあなたの時代は良かったですねと日々弾力を失っていく心。

たとえ良いテストが書けなかったとしても、テストを書こうとする意志をなくしたら終わり。