Slackでは品質エンジニアリングチームがE2Eテストをスケールさせる

SlackがCypressを使ったE2Eについて「Scaling End-to-End User Interface Tests」という記事を公開しています。単純なテストフレームワークの紹介だけでなく「Quality Engineer(直訳:品質エンジニア)」によるテストの啓蒙活動にも触れられていてとても興味深い内容です。この記事ではその内容をななめ読みしてみます。

Scaling End-to-End User Interface Tests

Slackにおいて「Quality(品質)」は関係者が共通で持つ責任になっています。「Quality Engineering team(QEチーム: 品質エンジニアリングチーム)」は、テスト文化、テストカバレッジの向上、高いレベルの機能を高速でリリースするサポートを担うチームです。

品質エンジニアリングチームは、すべての開発者がエンドツーエンドのテスト(E2Eテスト)をオーナーシップを持って書くための支援もしています。

Quality Engineering(QE: 品質エンジニアリング)とは、フレームワークを使ったり、ベストプラクティスを提供することで、再利用可能で、スケーラブルで、メンテンスコストの低いテストを作っていくことを指しています。

彼らが定める品質エンジニアリングは、一昔前に流行ったSoftware Engineering in Test(SET)にとても似ていますね。

SETチームのマネージャをしていたときに、日本特有なのか、たまたまなのかはわかりませんが、QA(品質保証:Quality Assuranse)チームやQAエンジニアの技術力の低さを課題として感じていました。

今後はSlackのように、品質保証から品質エンジニアリングへと、よりエンジニアリングに近い組織を作っていかなければ、アジャイル・DevOps時代には適用できないのかもしれません。

それでは前置きはここまでにsて、Slackが歩んだ品質の旅路を進んでいきましょう。

Finding a framework for end-to-end tests

SlackのE2E 自動UIテストは、HackDayという社内で開催したハッカソンからはじまりました。Cypress.ioを採用し、ハッカソン中にいくつかのテストがエンジニアによってすぐに書かれたそうです。

当時のSlackには、「どのようにテストを追加ていくか?」といったガードレールがありませんでした。その影響で、たくさんの重複した観点を持つテストが書かれてしまい、壊れやすいテスト(Flaky tests)になってしまっていたのです。

この残念な結果によって、テストはランダムに失敗し続け、テストのトリアージを進める方針へと変わっていきました。

問題や失敗は、チャンスともいえます!

QEチームは、Cypressのペインポイントを解決するためのよりよい方法を探す決意をしました。それはページオブジェクトモデル(POM:Page Object Model)の改良版ともいえるもので、UIとテストの間に抽象的なレイヤを作成したものです。

そして、1ヶ月のタイムボックスを切って、テストに対する新しい考え方を試しました(PoC)。

Abstract it all away

E2Eテストの読みやすさ、書きやすさ、メンテナンス性を考慮して、Slack特有のメソッドを作成し、ライブラリ化しました。抽象化されたレイヤによって、UIに対するアクションを特定の場所に集約できるようになりました。

Slackは従来のWebサイトと対象的な、今風巨大アプリなので、ページではなくコンポーネント単位に切り分けました。これによって、エンジニアはチャンネルサイドバーといったコンポーネントのインスタンスに対してテストを確認できるようになります。

ページオブジェクトの場合、以下のように先頭がページになることが多いですが、

pageName.clickSubmitButton().clickConfirmButton() ・・・・

記事で紹介されているコードは、コンポーネント(おそらくReactなどが内部的に持っているコンポーネントを指していて、画面の一部分を指していると思う)ベースになっていますね。

clien.channelSideBar.clickButton();
client.messagePane().clickButton();
client.messageInput().clickButton();

「UI抽象化 / ページオブジェクトモデルアプローチ」やJSのメソッドチェーンによって、コードの可読性や書きやすさも向上し、様々な問題が解決しました。

テストのガイドラインとしていくつかのベストプラクティスも発見しました。

  • 要素の選択はClassNameやIDを使わず「data-qa属性」を活用する。これによってJS/CSSの変化に強くする(参考:なぜE2Eテストでidを使うべきではないのか
  • 必要なときだけ新しいコンポーネントを作る。あらゆるUIを通したアクションを定期すべきではありません。Just-in-Timeですなぁ
  • コンポーネントとUIは一心同体で修正。コンポーネント間の影響も少なくする。たとえば、チャンネルサイドバーのコンポーネントは、全く関係ないはずのメッセージ入力と相互作用しないようにすべきです。
  • たくさんの小さなコンポーネントを作成するのではなく、意味のあるコンポーネントになるように分割していく。小ささよりも意味合いか。
  • UI抽象化はステートレスです(状態を持たない)。よって、テストでは状態を維持し、それに対して検証をしていく必要があります。

上記を考慮したコンポーネント設計については、UMLが図として紹介されていて、ページオブジェクトモデルの簡易版になっています。

  • Web Element: ボタンやテキストボックスと言ったUIを表現したもの。これに対してアクションを定義していく
  • BaseComponent: コンポーネント。これを拡張してクラスやコンポーネントを作っていく。コンポーネントはコンポーネントやWeb要素で構成される
  • BaseModal: モーダルを表したもの。モーダルはこれを拡張して作成される

Slackのクライアントアプリの場合、以下のようなコンポーネントにブレークダウンできます。

  • 左メニュー部分(スレッドやチャンネル一覧表示部分)
  • チャンネルのヘッダ部分
  • チャンネルのタイムライン部分、
  • メッセージ入力部分

記事ではサンプルとしてMessageInputComponentのコードが掲載されています。共通のメソッドを持ったBaseComponentを継承しており、data-qa="message_input" も設定されていますね。

ここがあんまり読み取れなかった。

From there, we pass in the selector for the MessageInputComponent and define what selectors live within the MessageInputComponent.

トップレベルのオブジェクトとしてClientと呼ばれるものを作成し、「hello world」を入力するステップは以下のように記述できるようになりました。

client.messageInput.typeMessage(‘hello world’, true);

Takeaways

以前の方法と今の方法を比べてみた場合、不安定さを60%削減できました。自動化エンジニアとフロントエンドエンジニアは、より簡単にテストを追加できるようになり、より多くのテストをメンテナンスできるようになりました。

私達のチームは、すべてのE2EをUIを抽象化した形にマイグレーションしているところです。不安定さとの戦いはまだはじまったばかり。一緒に問題を解決したい人は是非、Check out our open roles を見てみてね!

参考: Scaling End-to-End User Interface Tests by Slack engineering