フューチャー技術ブログ

アクセシビリティ情報を使った壊れにくいE2Eテスト

2/25の東京Node学園で発表してきました。

E2Eテストはみんなしていますか? 正直言うと、僕はあんまり好きじゃなかったです。お仕事では他の人が入れてくれたものが回っていたりしますが。前職で、SikuliXを使って社内向けのデスクトップアプリケーションのE2Eテストにトライしたことがあるんですが、すぐに壊れて動かなくなるので、費用対効果が出せなくてあきらめました(一人プロジェクトだったのもあります)。

  • 絵でマッチングを行うのでボタン画像をいっぱいスクショをとっていく必要がある
  • OCR機能はOSネイティブボタンの背景のグラデーションとかとの相性もあってあまりうまくいかず・・・自分で学習させるのも情報が少なかった。
  • ちょっとツールバーに新しいボタンを追加したりすると、絵が変わって動作しなくなる
  • ボタンを画像で置くが、QtはOSごとにネイティブなルック&フィールで描画するので、macOS用に書いたテストがWindowsで失敗する。両対応は2倍大変
  • macOSはDPIの違いがあっても常に二倍の解像度でスクリーンショットが取得できる(これをダウンスケールして表示してるみたい)ので問題が出にくいが、WindowsはDPI違いで取れるスクリーンショットの解像度が変わるので違うマシンで実行すると動かなくなる
  • 多言語化しても死ぬ

SikuliXをはじめとしたE2EテストのフレームワークはRPAみたいな自動化を後付けするのと同じような技術セットなので、すでに完成したアプリケーションに対して自動化をしたい、みたいな用途であればつまずく回数も減ってよかったのかもしれませんが、開発しながら利用するとメンテ工数が無限に消費されてしまいます。有名どころのGUIフレームワークに対応したfroglogic社のSquishを使うと良かったのかもしれませんが、一人プロジェクトの社内ツール向けに稟議をあげるのはちょっと難易度が高い価格。

この気持ちはいつか克服しないとな、と思っていたところで、この技術ブログで枇榔さんの記事でCypressを知って試してみました。Vue.jsのプロジェクトであれば、プロジェクト作成の時にオプションで入れられますし、自分で入れるとしてもnpm一発でインストールというのが良いですね。導入が面倒というのだけでもプロジェクトで統一的に使うときに放置されがちなので。

ここがよかったCypress

インストールの容易性、CIへの組み込みのしやすさなどは上記のエントリーでも十分に語られています。動画やスクリーンショットを残してくれたりするところも良いですし、テストランナーがそれぞれのシーケンスの途中経過を全て残してくれて、問題の分析がしやすいところも良いです。

「使いやすいテストランナー」はサーバー通信の記録も全て残してくれます。それだけではなく、モック機能を使って読み書きをハックすることも可能です。現在は裏のサーバーはDockerで認証のKeycloak、AWSエミュレータのlocalstackともども起動してE2Eテストしていますが、それでもテストしにくい部分はそのうちチャレンジしてみたいですね。

E2EテストはDOMのテストじゃない

いくつか不満はあります。環境設定でたくさんディレクトリとかファイルを置く必要があるもののそのサポートがない(vue-cliで作ったテンプレートから引っこ抜くのが一番楽)だったり(cypress init欲しいissueはあった)、Electron対応が途中で放棄されていたりとか・・・

一番大きいところは、やはりDOMを意識したテストにならざるを得ないという点ですね。
基本的にはSeleinumとかと同等で、タグのツリーをたどってテストする感じです。SikuliXの画像でやるよりは壊れにくいのですが、E2Eテストは「機能」のテストであって、DOMの単体テストではないはず。

人間がウェブサイトを見るときは「よしログインボタンを押そう」と判断しているわけで、「ヘッダーの中のclass=”login”な最初のDOM要素を探してきてクリックイベントを発行しよう」と思っているわけではなく、このギャップがE2Eテストの見通しの悪さに繋がっているんじゃないかと常々思っており、テストケースはもっと抽象的であるべき、と思っていて、「これはなんか違う」と思っていました。SikuliXの画像とかを大量に用意するテストよりははるかに情報量(バイト数)は少なく済むものの、まだまだ情報は圧縮されるべき。

E2Eテストは、人間以外の別のエージェントがブラウザ経由でウェブサイトを利用する、というユースケースです。で、本物のウェブサイトをブラウザの上から扱うエージェントというのがすでにあります。スクリーンリーダーです。じゃあ、アクセシビリティ情報を利用してテストを書けばより抽象度が高く、シンプルでわかりやすくて壊れにくいテストになるんじゃないかと思ったわけです。で作ってみました。

これを考えているときにたまたま読んでいたのがウェブアクセシビリティの本でした。視覚障害者の人たちが使うスクリーンリーダーとE2Eテストのテストランナーは似ており、ブラウザを操作するエージェントがいて、ユーザーはそれを操作します。スクリーンリーダー向けにメタ情報を提供するWAI-ARIAの拡張属性をE2Eテストでも活用すれば、E2Eテストの抽象度をあげることができるのではないかと考えました。

WAI-ARIAとは

HTMLはどんどんデザインの比率が上がってきており、タグ=意味の構造、CSS=デザインという本来の役割を守り切るのは難しく、UIデザインのためにdivタグやらspanタグを追加することはよくあるでしょう。そのようなデザイン重視のウェブサイトを、本来のセマンティックHTMLに近づける属性がWAI-ARIAです。ウェブサイトにアクセシビリティを導入する一連のタグ属性やルールなどです。

視覚障害者が利用するスクリーンリーダーは例えるなら、CSSを全部オフにした状態でウェブサイトを閲覧するようなものだと思います(僕は利用してないので想像ですが)。タグ構造にデザイン用の要素が満載なこの時代に、スクリーンリーダーというブラウザを外から操作する機械向けにいろいろな情報を教えてあげるルールを規定するのがWAI-ARIAです。

本来は<button>タグとテキストで表現されれば何も問題はないのですが、画像を使ったり、<div>タグを使ったりすることもあります。本来の役割(role)と、役割に対するラベル(aria-label)をあとから付与できます。これ以外にも、aria-selected(選択されていることを示す)とかaria-hidden(視覚要素でスクリーンリーダーからは隠したい要素)とか、さまざまな属性が定義されています。

<button>異議あり</button>

<input type="button" value="異議あり" />

<div role="button">意義あり</div>

<div role="button" aria-label="異議あり"><img src="objection.png"></div>

<div role="button"><img src="objection.png" alt="異議あり"></div>

<label id="objection-label">異議あり</label>
<button aria-labelledby="objection-label"><img src="objection.png"></button>

実装してみたCypressプラグイン

WAI-ARIAでは単に「ボタン」と表現されていても、↑のように実際のDOMの表現はさまざまです。それぞれごとにバリエーションを網羅してあげる必要があります。こんな感じでロールと名前を使って該当する要素をとってくるライブラリを作りました。

cy.aria('button.異議あり').click();

一瞬業務コードに入れて使ってみましたが、かなり便利でした。リポジトリを作って、button, textbox, slider, radio, checkbox, linkあたりのロールに対応し、これからもっと増やすぞ、と思っていた時に事件は起こりました。

Testing Library

実用的に使える機能を作り上げて、会社のGLとかにこんなの作ったぞ、と自慢した一週間後に、ほぼ同じようなことをするライブラリを見つけました。

https://testing-library.com/

これはDOMに対して要素を探し出したり、イベントを起動するというライブラリです。jQueryからDOMの編集機能を取っ払った感じですが、アクセシビリティの情報を使ってDOM要素をピックアップできます。このライブラリは多彩なアダプターがあり、ReactやReact Native、Vue.jsのユニットテストもできそうです。Enzyme + JSDOMみたいな感じで使えそうです。まだ使っていないけど今後使ってみたい。

このアダプターの1つにCypressアダプターがあります。いろいろコマンドが提供されていますが、findByRole()だけでほぼ済む気がします。というかこれしか使ってない。

cy.findByRole("button", { name: "意義あり" }).click();

この書き方で、前述のどのタグにでもヒットします。aria-labelledbyはなんかヒットしたりしなかったり不安定ですが・・・

Cypress + Testing Library導入の効果

かつて一世を風靡したかもしれないCucumberほどではないけど、ほぼほぼDSLのようなレベルになっていると思います。”button”とか”dialog”とロール名を書くところで、TypeScriptのコード補完が効かなかったのですが、型定義ファイルに対してpull request送って、それがマージされた新バージョンがすでにリリースされていますので、今はゴキゲンにテストが書けます。

cy.findByRole("button", { name: "編集" }).click();
cy.findByRole("dialog", { name: "ユーザー名編集").should("exist");
cy.findByRole("textbox", { name: "ユーザー名入力").type("yosuke furukawa");
cy.findByRole("button", { name: "保存" }).click();
cy.findByRole("cell", { name: "ユーザー名" }).should("have.value", "yosuke furukawa");

Cypressの中にはjQueryが内蔵されており、多種多様なセレクターが使えます。jQuery、最近は蛇蝎のごとく嫌われている風潮もありますが、便利で広く使われていた裏返しではあると思いますし、DOMを変更したりしないで文字通りクエリーのために使うにはいまだに便利です。Cypressもまさにその使い方です。

ただし、単にjQueryを使うだけではなく、SPA固有の挙動に最初から対応しており、ほとんどのケースで「処理待ち」のウェイトを入れなくても、タイムアウト(デフォルト4秒)するまで自動リトライしてくれるようになっていて、時間跨ぎのコードにもかかわらず、awaitなどを書かなくても良いし、時間稼ぎもしなくてよいAPIになっています。テストコードの中にワークアラウンドが顔を出してくるようなことがあまりなく(ゼロではないですが)、これもテストの見通しのよさに寄与しています。

cy.findByRole("button", { name: "異議あり" }).click();
// ↑ボタンが見つかるまで自動リトライ
// ↑見つかったら初めて実行

テストのTips

アクセシビリティ属性が必要なことをきちんと伝える

ウェブのデザインとフロントエンドのコーディングで担当を分けて行うことも多いでしょう。デザイナーの人には「アクセシビリティの情報は消さないで!」「なるべくセマンティックHTMLにして!」と伝えた方が良いですね。

マウスオーバーでCSSのみでふわっとボタンがポップアップしてくるテクニックはJSを使うよりもクールな気がします。また、テキストをCSSで差し込むとかもよくやるテクニックですね。しかし、アクセシビリティを考えると要注意です。Cypressでも:hoverで出てくる要素の存在テストってできないので。きっとスクリーンリーダーも苦手なんじゃないかと。

このあたりはデザインロールの人と丁寧にやっていく必要がありそうな気がします。

最初に存在チェックのテストを書く

存在チェックのテストをまず書いておくと、テストコードを書く速度があがります。

まず、このテストが通ることで、セレクターが間違ってないことが確認できます。「宣言的」は宣言が間違っていたときのフィードバックが弱いことが多いです。まず要素一覧を確認するテストがあると、実際に動くテストを書こうとして、期待通りに動かなかった場合の問題追跡がしやすくなります。指差し呼称ですね。

it("コンソールに必要な要素がある", () => {
cy.findByRole('button', { name: "ディバイディングドライバー射出" }).should('exist')
cy.findByRole('button', { name: "ファイナルフュージョン承認" }).should('exist')
})

あと、登場人物がすべてリストアップされていると、挙動のテストを書くときに、ここに書かれている要素をコピーして書けば良いので楽ですね。

なお、あとで使うからといって変数に入れても正しくは動きません。

ホットスポットのロールをどうするか

サーバーからとってきた動的な値を表示するテキストって、DOM上は単なるテキストなのでロールを持っていないのですよね。テスト上はここにロールがあって要素取得できるととても助かる。

次のどちらかな、と思っているのですが、どちらの方がスクリーンリーダーユーザーにとって自然なロール指定のかはちょっとわかってません。とりあえず前者にしています。

  • role=”cell”でテーブルのようにしてしまう
  • role=”textbox”で読み込み専用テキストボックスにする
{ /* role=cellでアクセス */ }
<div role="cell">{userName}</div>
{ /* role=textboxでアクセス */ }
<input readonly>{userName}</div>

まとめ

E2Eのテストコードは、「もっと短く表現できるはず」と昔から考えていましたが、それに近いものがCypress + Testing Libraryで実現できました。

アクセシビリティは、直接人間に対して効果を発揮するわけではなく、人間を補助するエージェントへの情報を増やすことで、結果的に人間を助ける、というのが今のウェブのアクセシビリティです。その特性はE2Eテストにも恩恵があります。

極端な例をあげれば、ボタンの名前が維持されれば、ボタンの位置がヘッダーにあろうが、サイドバーにあろうが、テストは壊れないのですよね。そのぐらい抽象度があげやすくなります。今まで、「E2Eテストは書く手間が大きく、なおかつ壊れやすく、コストパフォーマンスが悪い」と思っていましたが、だいぶ心理的な負担は下がり、最初からやっておけば、と思うようになりました。

当初は40分ぐらい発表できるネタとして考えていましたが、発表希望者多数により10分となってしまい、発表もだいぶ駆け足になってしまいました。後日、フューチャーの社外向け勉強会のFuture Tech Nightで改めて、Cypressについて初心者でもわかるように説明できれば、と思って企画中です。