リーダブルなテストコードについて考えよう ~VeriServe Test Automation Talk No.3~ 2022-07-27 での講演スライドです。
コンテキストとセマンティクスを意識してリーダブルなE2Eテストコードを書こうTakuya Suemura @ Autify, Inc.リーダブルなテストコードについて考えよう ~VeriServe Test Automation Talk No.3~2022-07-27
View Slide
おれは誰だぜ末村 拓也開発者、フィールドエンジニア、QAなどを経て、2019年にAutifyに入社。自動テスト、特にWebのE2Eテストに強い。現職ではテクニカルサポートを担当。テスト対象とテストフレームワークの互換性の問題などを調査、解決する役割を担う。テトリスが好き。
おれが今まで話してきたことJaSST'22 Tokyo 60分で学ぶE2Eテスト (ベリサーブ 伊藤由貴さんと共演)https://github.com/tsuemura/jasst22-tokyoJTF2020 テストを自動化するのをやめ、自動テストを作ろうhttps://speakerdeck.com/tsuemura/tesutowozi-dong-hua-surufalsewoyame-zi-dong-tesutowozuo-rou「E2Eテストとはなにか」「理想的なE2Eテストの書き方とは」をひたすら考えたり話したりし続けている人です
今日話すことそもそもE2Eテストってどういうものだっけ?E2Eテストをリーダブルにする理由は?どうやってリーダブルにするの?
そもそもE2Eテストとは
よくあるE2Eテストのイメージブラウザとかモバイルデバイスを自動操作してWebアプリとかモバイルアプリのUIをユーザーが操作するのと同じようにテストする
(Webの) E2Eテストで用いられる技術Webブラウザの自動操作技術Selenium, Cypress, PlayWright etc.要素特定の手段CSS Selector, XPath etc.
E2Eテストの例Cypressによる擬似コード/**ログインする **///メールアドレスを入力cy.type('input[name=email]', '[email protected]')//パスワードを入力cy.type('input[name=password]', 'pass1234')//送信ボタンをクリックcy.click('input[type=submit]')この例では CSSセレクタ で要素を特定し、それらをクリックしたり、文字を入力したりしている
E2Eテストをリーダブルにする理由は?
E2Eテストをリーダブルにする理由は?=(脳の)メモリの無駄遣いを防ぐE2Eテストコードは次のようなことを 想像 しながら読まないといけない長いテストコードを最初から読みながら今どのページにいるのか想像しながらどのボタンを押しているのか想像しながら想像をなるべく減らす のがポイント
読みにくいUI操作の例//送信ボタンcy.get('button[type="submit"]').click()//「OK」ボタンcy.get('button.primary').click()どちらも CSSセレクタ を用いて要素を探索しているが……type="submit"が送信ボタンであることを知っているのは エンジニアだけprimaryクラスがOKボタンに当たってるのは ただの実装上の都合ユーザーは type="submit"や .primaryのような内部的な属性値を使わず、 ラベル で探す読みにくいだけでなく、ユーザー目線でもないので、誰の得にもならない
読みにくいシナリオの例//メールアドレスを入力cy.get('input[name="email"]').type('[email protected]')//パスワードを入力cy.get('input[name="password"]').type('pass1234')//送信ボタンをクリックcy.get('button[type="submit"]').click()このページは ログイン ? それとも 新規登録 ?
読みにくいシナリオの例//新規登録ページにアクセスcy.visit('/register')//メールアドレスを入力cy.get('input[name="email"]').type('[email protected]')//パスワードを入力cy.get('input[name="password"]').type('pass1234')//送信ボタンをクリックcy.get('button[type="submit"]').click()直前で「新規登録ページにアクセスした」という 文脈 が無いと読み解けない
ここまでのまとめ想像で読む部分を減らしたい、そのためにユーザーが要素を探すときと同じ方法で要素を探したい文脈に依存する書き方を減らしたい
どうやってリーダブルにするの?
どうやってリーダブルにするの?セマンティックな書き方を用いるユーザーにとって意味のある書き方を用いるコンテキストを明示する「今何をしているのか」「今どこにいるのか」を明確にする
読みにくいUI操作の例(おさらい)//送信ボタンcy.get('button[type="submit"]').click()//「OK」ボタンcy.get('button.primary').click()どちらも サイトの内部構造 を用いて要素を探索しておりユーザー目線 ではない意味のある = セマンティックな書き方 を使おう
セマンティックな書き方の例1. 文言を用いる2. サイトのアクセシビリティを用いる
1. 文言を用いるCypress では文言を用いたセレクタを使える以下の例では Sign Upという文言を含む要素をクリックするcy.contains('Sign Up').click()※複数見つかった場合、一番最初に見つかった要素をクリックしてしまうので注意
文言だけでは出来ないケースはどうしたら?例: ハンバーガーメニューのアイコン例: あるラベルを持つ入力フォーム例: 画像
Testing Library を使ってみようTesting Library要素の 役割 や ラベル などを用いてテストコードを書くためのライブラリ//例getByRole("textbox", {name: /メールアドレス/})
Testing Library を使わない場合// spanタグを用いて作成した擬似的な SubmitボタンSubmit// buttonタグを用いて作成した SubmitボタンSubmitこの2つは buttonという role と Submitという nameを持つ意味的にはほぼ等価だがテストフレームワークからは異なるセレクタを使わなければいけないcy.get('span').contain('Submit')cy.get('button').contain('Submit')
Testing Library を使う場合Testing Library を使うとどちらも同じ形で書けるgetByRole("button", {name: /Submit/})
アクセシビリティの高いサイトはテスタビリティも高いTesting Library は アクセシビリティ を用いてテストしているつまり、これら3つがシームレスに実現できる開発者: アクセシビリティ改善QA: アクセシビリティ特性を用いたユーザー目線でのE2Eテストユーザー: アクセシビリティの利用テスターにとってのアクセシビリティの優先度は実は高いテストしにくいサイトがあったときに、「テストしやすく」ではなく「アクセスしやすく」という提案が出来るかも
どうやって使うの?Testing Library は Cypress, Puppeteer, TestCafe, PlayWrightなど主要なテストフレームワークに対応簡単に試したいならChrome拡張 Testing Playground を使おう
コンテキストを明示する
コンテキストを明示する読みにくいシナリオの例(おさらい)//メールアドレスを入力cy.get('input[name="email"]').type('[email protected]')//パスワードを入力cy.get('input[name="password"]').type('pass1234')//送信ボタンをクリックcy.get('button[type="submit"]').click()このページは ログイン ? それとも 新規登録 ?
コンテキストを明示する手法1. Page Object2. Context Enclosure
Page Object Pattern の利用ページ内のロケーター、ページ特有の操作などをオブジェクトにまとめるテクニック本来はメンテナンス性向上のための技だが、副次的にコンテキストを明示することも出来るconst loginPage = new LoginPage()loginPage.getEmailInput().type('[email protected]')loginPage.getPasswordInput().type('pass1234')loginPage.getSubmitButton().click()どのUI要素も loginPageという Page Object のインスタンスから生えている= ログインページ内の要素であることが明示的に示されている
Page Object の実装例class LoginPage {getEmailInput() {return cy.get('input[name="email"]')}getPasswordInput() {return cy.get('input[name="password"]')}getSubmitButton() {return cy.get('button[type="submit"]')}}ログインページ内の要素をあらかじめ PageObject 内に定義する
Page Object は結構手間がかかる例えば、ログインページに Remember me?というチェックボックスを追加したが、Page Objectには追加していないとするconst loginPage = new LoginPage()loginPage.getEmailInput().type('[email protected]')loginPage.getPasswordInput().type('pass1234')cy.contains('Remember me?').check() //ここだけ loginPageに属してないように見えるloginPage.getSubmitButton().click()要素を追加した際、かならず Page Object に要素を登録する必要があるPage Object はコンテキストを明示する目的に対しては 重い アプローチ
Context Enclosure現在のコンテキストに応じてスクリプトの一部を囲うPage Objectよりも「コンテキストを明示する」という意図が明確になるcy.visit("https://demo.realworld.io/#/register"); //新規登録ページに遷移//この部分が Context Enclosurecy.onRegisterPage(cy => {cy.findByPlaceholderText("Username").type("foobar")cy.findByPlaceholderText("Email").type("[email protected]")cy.findByPlaceholderText("Password").type("Pass1234")}※名前は先日考えたのでググってもろくなのがでてきません実装のサンプルは https://zenn.dev/tsuemura/articles/13b0ea44c1a20a
Context Enclosure の実装方法Cypress.Commands.add("onRegisterPage", (fn) => {fn(cy);});1ページにつき3行で実装でき軽量Cypressの場合はカスタムコマンドで実装する
コンテキスト内でのみ利用できるコマンドCypress.Commands.add("onRegisterPage", (fn) => {Cypress.Command.Add("showMessage", (message) => { //独自コマンドの定義cy.log(message)})cy.url().should('include', 'register') // registerページにいることを確認fn(cy);});onRegisterPageの中でだけ利用できる showMessageというコマンドを定義した例えば loginや fillCredentialsのようなhelperを定義してあげるとテストコード記述が楽になる同時に onRegisterPageが呼ばれた段階で registerを含むURLにいることを確認している
Context Enclosure の利点最低限の実装であれば各ページ3行ぐらいで済むので楽全てのページに実装するのもそう大変ではない「あるコンテキストにいる」という検証をセットで実装できるコンテキストに応じて独自のコマンドを実装できるContext Enclosure の欠点こないだ考えたばっかりなのであんまり枯れたアイディアではないこと
今日ほとんど Context Enclosure の話をしに来たんでフィードバックもらえると助かります
まとめ
まとめ悩まずにテストコードを読み書きするためにリーダビリティに気を使うそのためにセマンティクス(≒アクセシビリティ)とコンテキストの2つを紹介したユーザー目線で、文脈が明確なテストコードを書こう
Enjoy Testing!スライドか?欲しけりゃくれてやるぜ……探してみろ 今日の発表資料の全てをSpeakerdeckに置いてきた(Twitter / Zoomのチャットとかでも共有されると思います)