見出し画像

Cypress をお供にE2E受け入れテスト駆動開発 〜そしてAutifyへ〜

E2Eテストの自動化、それは人類の夢。

こんにちは、こちらはLinc'wellアドベントカレンダーの9日目です。

さて、E2Eテストの自動化、皆様は実施されておりますでしょうか?
していないですね、分かります。

E2Eテストは実際のユーザの体験に最も近いテストであり、重要性は高いにも関わらず中々開発プロセスに組みこむ難易度が高いです。

私はE2Eテストとは「メンテナンスコストの高さ」と、そこから来る「このテストメンテする意味ある…?時間かかるばかりでめっちゃ壊れるじゃん…」という気持ちとの戦いだと思っています。

と言う訳で取り組んでみたのがATDDインスパイアの受け入れテスト駆動開発なのですが、その取り組んでみた結果と今後についてこの記事では語ろうと思います。

受け入れテストとは

始めに受入テスト駆動開発ってなんやねんというWhatの定義をしてから、なぜそれをE2Eで回したいのかというWhyの話をします。まず本文書では受け入れテストを次の二つの意味から定義します。

1. 意味のあるゴールが定義された、ユーザが行う一連のタスクを、実際のユーザのインタラクションを用いて再現することによってテストすること
2. その条件をステークホルダーと合意したもの

まず一つ目について、具体例を出すと一般的なECサイトを作っているとします。ECサイトでは代表的なCVは商品を購入することです。なのでそれに至るまでの流れをシナリオとして書き出すと次のようになります。

画像1

この場合 `意味のあるゴール` とはもちろん「商品の購入」ことです。そして、このゴールを達成するために実際のユーザが体験するであろう操作の流れを定義したもの + ゴールを達成することが受け入れ条件で、それを確認することが受け入れテストです。

そして、もう一つ重要な側面がこの受け入れ条件をステークホルダー(この機能開発に関わる人々 - コンテキストによって変わりますが実際のユーザやPdMやその会社の経営陣など様々な人を含みます)と合意がされていることです。これがなぜ重要かについては次に項で語ります。

受け入れテスト駆動開発とは

まあ名前の通りなんですが、プラスこれをE2Eテストを書きながらシナリオが動くように開発して行くことを「受け入れテスト駆動開発」とここでは定義します。

発想はATDDから来てますが、私がやっているのは厳密にはATDDには従っていない(ATDDでは受け入れ条件を技術者でなくとも理解できる言語に落とし込んで、それがテストコードとして動くようにするが、私はそれをやっていない)のでこんな呼び方にしてみました。

なぜ受け入れテスト駆動で開発するのか

目的は次の3つです。

1. 初めに満たすべきシナリオを具体化することによって、ステークホルダー間のズレを無くす & それを作業の「完了」の定義とするため
2. 常にユーザに対して価値を提供しているものを開発することによって漸進的にソフトウェアを改善するため
3. シナリオテストを手動で回すコストを減らす

記事としてはE2Eテストをメインのテーマとして書くつもりなんですが、取り組みとしては一番満たしたいのは一つ目の点で2個目と3個目はそれの結果副次的にもたらされるものという建てつけです("副次的"とは言ってもかなり重要ではありますが)。順に説明します。

1. 初めに満たすべきシナリオ + ゴールを具体化することによって、ステークホルダー間のズレを無くす & それを作業の「完了」の定義とするため

スクラムの文脈で受け入れ条件をPOとスクラムチーム間で握ったりすると思いますがアレのことです。

仕事の基礎ではありますが、まず我々がなにかを実装するに際してなるべく思い込みを排除して「何を作るのか」を明確にする必要があります。これを行うためにステークホルダーと協力して受け入れテストを設計します。具体的なシナリオを考えることによって初めて考慮が漏れていた状態などを洗い出すことができることが多々あるでしょう。

また、個人的にここで大きいのはこのシナリオを自動テストにすることによって、誰の目から見てもその機能の実装を明らかな「完了」にできるという点です。

私の好きな本である Clean Coder という本に次のような一節があります。

プログラマが陥る行動の中で最もプロとしてふさわしくないのは、作業が終わっていないのに終わったとウソをつくことだろう。これはあからさまなウソだ。最悪である。だが、目立たないウソもある。それは、自分勝手に「完了」の定義を作ろうとすることだ。充分にやったと勝手に納得して、次の作業に移ろうとするのだ。残った作業は時間のある時に対応すればいいと、自分勝手に正当化するのである。

私はこれかなーり耳が痛くて、ある程度画面や機能を作ったら「今のところはこれで一旦OK」みたいに勝手に判断して「今自分が作りたいもの」の開発に移ることが多々あります。ですが、開発プロセスに「ステークホルダーと握った完了の定義を満たすシナリオを自動化する」ことがルールとして組み込まれていれば、その結果を持って完了しているかどうかが明確に分かるようになります。

実際に自分も最近E2Eを書いていて思うのですが、ちゃんと通しでテストを書くことによって結合が不十分であったり、状態の考慮が不十分であることに気付きやすくなっています。

あまりかっちりし過ぎて組織として余白がなくなるのも考えものですが、終わっていたと思われていたものが実はまだやることがあった、という状態がステルスで積まれていくのはプロジェクトを進める上でクリティカルになりかねないので、ある程度の規律としては持たせてもいいのかなと考えています。

E2Eというと単純にテストの工数削減に着眼されがちですが、こういった仕事を補助するものとしても大きい価値を提供してくれるのでないかと考えています。

なぜE2Eテストの自動化をCypressで行うのか

なぜE2Eで受け入れテストを自動化しながら開発していきたいのかを確認したので、次に実装について話していきます。

E2Eを行うためのツールは色々あります。Selenium、TestCafe、Pupetter…

その中でなぜ私はCypressを選んでいるのかという話ですが、理由は一つで「開発プロセスに溶け込みやすいから」です。(あと正直言うとCypressで満足してしまったため、その他で本気で試したことあるのがSeleniumだけなのでTestCafeとかももしかしたら体験いいかもしれないです)

具体的に述べると、Cypressでは一度プロセスを立ち上げてテストを指定すると、テストコードを更新する度にホットリロードがかかって自動でテストを実行し直してくれます。このスピード感のおかげで開発しながらテストを通すようにするというのがそれほどストレスなくできます。

Cypressのサイト内の紹介動画を見るとイメージつきやすいと思うので、よかったら見てみてください。

2年くらい前の体験で他の技術を批判するのはアレなのですが、昔Selenium + webdriver.ioでE2E開発していた時にはお世辞にも体験がいいとは言いづらいものでした。理由としてはとにかく遅いです。これに尽きます。実際のコードやテストコードを修正しながら開発というのが大分厳しいので、どうしても開発するフェーズとE2Eを書くフェーズをきっちり分けて書くようにしなければなりませんでした。

ここのDXが悪いと、メンテナンスされ続ける可能性が下がり、先ほど挙げた第一の目的には致命的な影響を与えるため、この点においてCypressを選んでいます。

脇道: クロスブラウザテストに関する私見

若干オフトピックですがCypressを選ぶ際にはSeleniumなどと比較してクロスブラウザテストを捨てることを前提としているため、これに関して自分の想うところを書きます。

次の2点でクロスブラウザはやる価値を見出しづらいと考えています。

- クロスブラウザを対応しようとすると多大なコストがかかるため
- 第一の目的には不要であるため

まず現状を確認すると、CypressでテストできるのはChromeのみです。一応FireFoxやIEへの対応などのissueも立っているため将来的にはサポートされると思います。
Proposal: Support for Cross Browser Testing · Issue #310 · cypress-io/cypress · GitHub
Cross Browser issue tracker · Issue #3207 · cypress-io/cypress · GitHub

これに対してSeleniumではほとんどの主要なブラウザに対してdriverが存在するため、クロスブラウザテストを行うことができます。

クロスブラウザテストの自動化の何が大変なのか

「じゃあSeleniumにした方がお得では?」と思うかもしれません。私も昔はそう思っていました。思っていましたが数々の闇を見たため気軽には推奨しなくなりました。

私が出会った中で記憶に残っているものとしては「Firefoxで Drag & Drop ができない」とか「Edgeで click が動かない」あたりです。こういう時はだいたい次のように JS を inject するようなワークアラウンドを取っていました。(取っていましたがこれだと実際にクリックできない要素に対してもクリックイベントを起こせてしまうため、ユーザ視点でのテストとしてはアレです。)

// クリックイベントを直に送り込む
browser.click = (selector) => browser.selectorExecute(selector, function(element) {
 element[0].click();
});

あと最近見かけたものとしてはこの記事が記憶に新しいです。
クロスブラウザでテキストフィールドの内容をクリアするまでの道のり | Autify

おそらくクロスブラウザテストの自動化に取り組んだ方々、それぞれ思い思いの闇を抱えていらっしゃるのではないかと思います。こういったものが一個起きるだけでも原因調査からのワークアラウンド実装で1日2日普通に持っていかれるので、とても開発の片手間でできるようなものではないです。

クロスブラウザテストで何を確認したいのか

あと先に挙げたように受け入れ条件を満たすテストを全ての環境で行う必要があるのかと言うとそうでもないと思います。
そもそもクロスブラウザテストで何を確認したいのかと言うと

ブラウザ・OSによってJS及びCSSの実行環境が違うため
- あるブラウザでは存在しないJSのメソッドを読んでしまい、エラーが発生しないかを確認したい
- CSSの違いにより、大きな表示崩れがないかを確認したい。

なのではないかなと思います。

JSの違いに関しては、受け入れテスト全部回すのは too much なのではと言う気持ちがあります。最近だとIEやiOSの低いバージョン(9とか)を対応するところも減ってきていると思うので、そうなるとJSの違いが問題になることはほとんどないと思います。もちろん全部回した方が安心なのは間違い無いんですが、先ほど述べたクロスブラウザテスト自動化にかかる工数よりは削減できる工数の方が下回るだろうなと思います。

また、CSSによる見た目の違いに関してはそもそもE2Eでは検知できないです。E2Eのついでに操作ごとの画面のスナップショット取って visual regression test とかもやるとは思いますが、そこで確認できる regression はあくまで同一実行環境による前回のテストとの違いであり、他の実行環境の比較ではありません。

もちろん他の環境との比較をするようにできなくはないでしょうが、単純な Pixel Perfect な比較では微妙な見た目のニュアンスの違いで一々違うものとして報告されるのでそこまで意味がない気がしています。結局目検での確認は必要で、ポチポチしなくて済むだけマシかもしれないですがそれはクロスブラウザテストを頑張って対応するほどの価値ではないなと。

まとめ

これらから基本的にクロスブラウザテストを対応するのはペイしない、むしろマイナスになる可能性すらあるため私としてはやらなくていいかなと考えています。

開発プロセス

次にE2Eをどうやって開発プロセスに入れているのかについて触れていきます。
大まかなプロセスとしては次のような感じです。

1. タスクを洗い出す
2. バックエンドを一通り作る
3. フロントのViewだけを作る
4. Cypressを回しながらフロントの実装を作っていく

※ このプロセスはフロントとバック両方を一人の人間が作ることを前提としたプロセスになっています。なのでチームによっては最適なやり方は違うと思うのであくまで参考程度に考えてください。
※ あとAPIのスキーマ設計とDB設計は最初に大方設計しきってから実装していっています。考慮漏れ・違いがあるので後から修正することも多々あるのですが、ここに関しては段階的に作っていくよりは一気に作る方が効率がいいと考えました。

1. タスクを洗い出す

最初にこういうのを実装していけば終わりそうというタスクを洗い出します。もちろん実装してみて初めて分かることもあるのでそんなに厳密にはやらないです。ここでの目的は後ほど結合前に個別にガッ!と作った方が効率良さそうなところを見つけるのと、単純に見積もりのためです。

2.バックエンドを一通り作る

次にその機能で必要なバックエンドの機能を一通り作ります。結合しながら段階的に作るのではなく、一気に作ってしまう意図としては、スキーマが定義してあってあとはユニットテストさえ書けばそこで閉じた動くものが作れると考えているからです。細かいエラーハンドリングとかは考慮漏れが起きることがありますが、E2Eを作っていく時にあまりストレスなく開発したいという意図でこうしています。

3.フロントのViewだけを作る

次にフロントのViewだけを作っていきます。"Viewだけ"というのは遷移ロジックやAPIコールをするイベントなどを発火するロジックなどを書かず、見た目だけのコンポーネントを作っていくという意味です。これも同じく「E2Eを作っていく時にあまりストレスなく開発したい」という意図です。

4.Cypressを回しながらフロントの実装を作っていく

さていよいよ結合ですが、ここでCypressをおもむろに立ち上げてテストが通るように結合部分のロジックを書いて行きます。

ここで気をつけている点がいくつかあります

PageObjectは初手で作りながら開発する
E2Eテストの文脈ではよく知られた言葉ですが PageObject というページをオブジェクトに見立てて、そのページでユーザが行うアクションを関数として定義したものが PageObject です。

実際のコードとしては次のような感じです。トップページからログインしたり、プロフィールページに行く動作を関数として定義しています。

export const TopPage = {
 login: () => {
   cy.get('[data-cy=show-login-form]').click();
   cy.get('[data-cy=login-email]').type('test@example.com');
   cy.get('[data-cy=login-password]').type('password');
   cy.get('[data-cy=login-submit]').click();
 },
 goToProfile: () => {
   cy.get('[data-cy=go-to-profile]').click();
 }
};

テストコードの方では単純に import して TopPage.login() と呼んであげるだけです。

普段書いているアプリケーションのコードは安易な共通化は気をつけた方がいいですが、PageObjectのメソッドは文脈もクソもないので共通化してなんぼです。「ここのこの動作なんてこのテストでしか使わんやろ〜」と絶対の確信があるもの以外は最初からPageObjectのメソッドとして作った方がいいと思います。

要素を選択する時は data-cy 属性を付与して行う
これは正直どう書くのか迷い途中なんですが、今のところ `data-cy` というテスト用の属性を付与して要素の選択を行うようにしています。利点としてはまず選択のロジックを書くのが非常に簡単です。

cy.get('[data-cy=go-to-profile]').click();

これだけで済みます。また簡単な変更に強いというのもあります。例えば対象の要素の文言やクラス名が変わったとしてもテストは壊れずにその要素を選択することができます。

ちなみに `data-test` の様に汎用的な名前でなく `data-cy` という名前にしている意図としては、むしろあえてこの選択用の属性は cypress 用のものだと明示したいという意図があります。現状微塵も書いてないんですが、将来的に jest などでユニットテストを書く時に同じtest用に意味が混ざっちゃうのが嫌だなという意図です。(書いてて思いましたが、あまり意味が混ざったせいでテストが壊れる具体例もパッと思いつかないのでもしかしたらOKかもしれないです。)

迷いどころ
ただ、迷っているというのはJSテスト界隈で著名な Kent C.Dodds さんはテスト用の data 属性を使わずに実際のユーザが画面を見て操作するのと同じ見つけ方を再現することを推しているからです。(テストが安定しない場合は `data=testid` みたいなのも使っていいよと言ってはいますが)
Making your UI tests resilient to change

これ何となく言っている意味は分かって、テスト用の属性を付与するのが実際のユーザがその要素を発見するプロセスとは違うのでそこはかとない気持ち悪さはあります。ただ、正直自分の中では文言変更に対する弱さとSelector実装の地味な面倒さを考慮すると、その欠点を超えるほどにこの気持ち悪さを言語化できません。このため私はテスト用の属性を付与するやり方で行なっています。

後日談
とは言えこの記事公開後にこんなこと思ったりはした

ちなみに、なるべく実ユーザと同じ要素の選び方をしたいという場合は、これまた Kent C.Dodds さん作の cypress-testing-library を使うと実装しやすいです。
GitHub - testing-library/cypress-testing-library: 🐅 Simple and complete custom Cypress commands and utilities that encourage good testing practices.


ぶっちゃけ上手くいってるの?

いってないです。以下反省点です。

たまに個別のモジュールの実装をどんどん進めたくなっちゃう

いきなり完全に個人の問題なんですが、一番の問題であったと思うので最初に持ってきました。

私の性質として「成果が目に見えやすいものをどんどん作りたい」というものがあります。
どういうことかと言うと、シナリオを通すような線を作るのではなく、単一のページをとにかく作っていく方が楽しいということです。プロとして最低な発言かもしれないですが、線を通すインテグレーションのロジックは中々に骨が折れるし、もしかしたら一番設計に頭を使う部分かもしれないのでついつい先延ばしにしたくなってしまいます。

この理由から結合して書くのを先延ばしにすることが多々あってちゃんとE2Eを開発プロセスに組み込むというのができていませんでした。性格的な問題な気がするので、いい感じに仕組みでカバーしたいところですが、これについては後ほど書きます。

プロトタイプの忠実度について

「常に動かすものを」の目的としてユーザやステークホルダーからのフィードバックが受けやすいようにと言う目的を掲げていますが、別にこの目的を果たすためであれば実装は必ずしも必要ではありません。

プロトタイプには忠実度という概念があります。「デザイナーのためのプロトタイプ入門」と言う本から引用すると

「忠実度」は、プロトタイプの見た目と動作が最終製品にどれくらい近いかを表す。

基本的に忠実度はそれを準備するのにかかる時間と比例します。実装はかなり忠実度が高い部類に入ります。

スクリーンショット 2019-12-09 0.13.58

仮説を検証したい時にはもっと忠実度が低い方法がいろいろ取れて、例えば紙に書くだけでも十分なこともあるだろうし、いわゆるプロトタイピングツールを用いて作ることも可能です。

「ここは絶対変わらんやろ」みたいな導線はいきなり実装してもいいとは思いますが、結構この「どのタイミングでどの忠実度で作るか」というのは難しい命題だなと感じたので、これに関してはいろんな媒体でのプロトタイピングの腕を磨きつつ言語化していきたいなと思いました。

余談ですが、この観点で十分に仮説検証の時間を取らずにいきなり実装に入ってしまうスクラムのプロセスには疑問があって、ユーザリサーチのスプリントと開発のスプリントを明示的に分けるデュアルトラックアジャイルのようなやり方で進めた方がいいのかもと考えています。

Dual-Track Agile: Why Messy Leads to Innovation - Mind the Product

要件が変わりやすい時期には足枷になる

よく新規開発でスナップショットテストや Visual Regression テストを導入しようか迷っている時に「今は仕様やデザインが固まりきってないからむしろ入れない方がいいよね」みたいな議論がなされることがあると思いますが、それです。

今私が作っているものは新規開発のフェーズで、要件に不確実性を持ったままテストを作って壊れていくというのがありました。もちろんそういった不確実な部分を明らかにしていくのが目的であり、取り組みとしてはいいことだと思うのですが、テストを書くのが早過ぎるタイミングもあるのかもなと感じました。

今後について

最後に、今後変えて変えていくことについて語ります。

同僚に詰めてもらう
まず最初に、週に2回くらいの定例を設けて一緒のチームで働いている開発者に詰めてもらおうかなと思っています(彼は今ドイツ旅行に行っているのでまだ相談していないですが)。

現状ほぼ一人でゴリゴリ開発しているため、結構タスクの順番が自分で規律持たないと制御できない状態になっています。なので、かっこいい感じで表現すると自分の仕事の進め方に対してフィードバックもらう機会を定期的に入れるのが第一歩かなと思いました。

ただ、ストレスになってメンタルにダメージが蓄積される可能性もあるのでその時はまた別の打ち手を考えます。

プロトタイピングの腕を磨く

先ほども触れましたが「何を実装するのかを決める」ことは今回の大目的である「ステークホルダー間のズレを無くす & それを作業の「完了」の定義とするため」とは別立てで考える必要があると感じました。

考えてみましたが、まあひたすら場数踏まないとどうしようもなさそうだなという気がしているので、初速下がるかもしれないですが一旦紙とfigmaでプロトタイプを作って合意取ってから実装する内容決めるというのをやってみようかなと思います。

Autify 導入します

Cypress使っていましたが、今度Autifyを試すことになりました。(一応否定しておきますが私はAutifyの回し者ではございません)
Autify自体の詳しい説明は省くの詳しくはAutifyのサイトを見ていただければと思いますが
Autify(オーティファイ), AIを用いたQA自動化プラットフォーム

使用感としては Selenium IDE みたいな感じで画面をポチポチするとテストができるという感じです。

Autifyに期待していることは2点で「クロスブラウザテストが安定して動くこと」と「テストのメンテナンスが容易であること」です
散々クロスブラウザはペイしないと言いましたが、誰かがその業を背負ってくれるのであれば話は別です。一回テスト書けば対応したいプラットフォーム全部でテスト動いてくれるのであればやらない理由はありません。苦労が偲ばれますがこれを現実にしようと決断してくれたことにただただ感謝です。

また、Cypressは大分開発しやすいのは間違い無いですが、やはり先に挙げた PageObject を拡充させたり data 属性を使った運用など、これから自分以外の誰でもメンテできるようにしたいことを考慮すると運用に不安があるというのが正直なところです。なのでこの辺の不安を取っ払ってくれることを期待して導入することになりました。使いたおすぞ!

おわりに

結構自分はE2Eに対して想いがあって、それはE2Eテストが素早く回せるようになればリリースの頻度が上がってより良い価値を提供しやすくなるというのが一点、そして不幸な仕事がなくなるのが一点です。

特に後者に関して、今まで働いてきた現場では「若手に仕様を理解するためという名目をつけてポチポチテストさせる」とか「外部のテスト会社さんに大枚払ってポチポチしていただく」というソリューションで解決?するのを見てきました。テストを設計したり自動化するのはクリエイティブな仕事だと思いますが、画面をひたすらポチポチするのはその人のキャリアに取って何もプラスにならない無駄な時間であると感じています。

こういった仕事を少なくとも自分が働く職場ではなるべく発生させたくない(し、やりたくない)なという思いがあるため、今後もE2Eと開発プロセスについては探求して行きたいと思います。長文ですがお読みいただきありがとうございました。

この記事が気に入ったらサポートをしてみませんか?