質の高いUIテストのための5か条:よくないプラクティスは時間とお金を奪い涙をもたらす

この記事はJim Holmesによるゲスト投稿です。

テスト自動化技術、特にユーザーインターフェイス(UI)またはエンドツーエンドレベルでの自動化技術は、チームの生産性、有効性、士気に大きな影響を与える可能性があります。質の低いテストスイートは膨大なケアとメンテナンスを必要とするため、最終的にはテストスイート全体が実行されなくなったり無視されたりすることになります。

質の低いテストスイートはチームの能力に対する組織の信頼を損ない、品質の高いソフトウェアを提供する能力がないという印象を与える可能性があります。こう言うと、あまりに大げさな誇張のように思われるかもしれませんが、そうではありません。組織のリーダーは、利点が明確ではないテストの作成に多くの時間が費やされていると考えています。さらに、組織のリーダーの目から見ると、テストは失敗しつづけ、脆弱なテストスイートのメンテナンスに多大な時間が費やされているいっぽうで、チームが出荷しているシステム/製品にとって顕著なメリットはありません。

もっとよい状況にできるはずです。

いくつかの簡単なアイデアを注意深く考慮して組織的に実践すれば、テスト自動化技術を利用するチームに多大な利益がもたらされます。この記事は、主にUIテストの自動化を対象にしていますが、いくつかのアイデアは他の多くのタイプの自動テストにも適用できます。

1:質の低いテストコードを書かない

テストコードは製品コードです。復唱しましょう――「テストコードは製品コードです」

テスト自動化コードを書くときは、価値、明快さ、保守性を意識する必要があります。これは、優秀なチームがWebサービス、データアクセス、セキュリティを実装するコードを書くときに熱心に心がけることと同じです。経験豊富なチームは、いっときの時間短縮のためにずさんなコードを書くことの長期的な代償について、痛い経験から教訓を学んでいます。熟練したソフトウェア組織は、米海軍特殊部隊SEALsのスローガンの意味をよく知っています。「ゆるやかはなめらか、なめらかはすみやか」です。

あなたがシステムコードを書くとき注意を払う(払っていることを願います!)のと同じように、テスト自動化スイートを書くときも注意を払う必要があります。つまり、優れたシステムコードを書くための原則とプラクティスをテストコードにも適用する必要があります。残念ながら、コーディングに不慣れで重要な基礎力に欠けるテスターが UI テスト スイートを作成する例が非常に多く見受けられます。

チームが価値の高い安定したテスト スイートを作成できるようになるための最良のアプローチの1つが、テスターと開発者をペアにして作業させることです。協力してテスト自動化コードを作成することには、以下のように確実で目に見える利点がいくつもあります。

  • コードがより簡潔になる。複雑さは致命的な問題です。一般的に、あまり経験がないテスターはこの点を理解していないため、多大なメンテナンス作業が必要になる複雑すぎるテストを作成する傾向があります。
  • 重複が減る。重複のコストは高いものです。低品質のコードがソースコード全体にコピーされているのは危険です。Don’t Repeat Yourself (DRY) 原則は、ソフトウェアエンジニアリングの領域で長年にわたって繰り返し強調されるプラクティスの1つです。優秀なソフトウェア開発者はこの原則を頭に叩き込んでおり、本能的に実践しています。
  • 意図が明確になる。わかりにくいコードはメンテナンスが困難です。コードが何を行っているのかわからなければ、コードを修正、改良、拡張することもできないでしょう。優秀な開発者は、わかりやすい命名規則や構成など、わかりやすいコードを書く方法をテスターに教えることができます。

開発者とテスターをペアにすると、開発者側のテストスキルの向上につながるという利点もあります。

2.ロケーター戦略を改善する

おそらく、ロケーターはUIテストツールの動作を理解するうえで一番重要な側面と言っても間違いではないでしょう。ロケーターは、ツールが画面、ページ、ビュー、ウィンドウなどにあるアイテムを探す方法を指定します。ロケーターによって、ボタンを探してクリックしたり、フォームのフィールドにテキストを入力したり、レポートから値を抽出したりといった処理が可能になります。

厳密なロケーターの動作メカニズムは、テストツール (たとえばRanorex StudioかRanorex社のベータ版WebテストツールWebTestItかなど) によって異なりますし、アプリケーションの表示テクノロジー(HTML、Silverlight、Windows Forms、Oracle、SAPなど)によっても異なります。さらに、厳密なロケーターの構築には、アプリケーションの全体的な設計や技術スタックが影響します

(補足:「ロケーター」という用語は、アプリケーションのUIテクノロジーや、テスト自動化に使用するツール、さらにはチームによっても、違った使い方をされることがあります。また、ロケーターは、オブジェクト識別子、要素識別子、検索ロジック、アクセシビリティ識別子と呼ばれることもあります。この記事では、厳密な用語の定義にはこだわらないようにしましょうーーそれより重要な根本的概念のほうに注目してください)

ここでは、コンタクト管理システムのテストを作成していると仮定しましょう。次のような画面を使用した新規コンタクト先の作成機能周りのチェックを自動化する必要があります。

テストスクリプトは以下のステップを実行する必要があります。

  1. ページをロードする
  2. コンタクト先作成ボタンをクリックする
  3. 表示されたポップアップダイアログのフィールドにテキストを入力する
  4. ボタンをクリックしてダイアログを送信する
  5. グリッドで新規レコードを探し、正しいかどうかを検証する

最初にページをロードした後のすべてのステップでロケーターを使用します。いかに適切にロケーターを処理するかは、安定性の高い、価値のあるテストを作成するうえで非常に重要です。

一般的に、ロケーターはUI上の要素やオブジェクトの属性またはメタデータに基づきます。HTMLでは、IDやNameなどのプロパティにアクセスできます。上記の画面ショットにプロパティの一部が表示されています。iOSアプリケーションには、自動IDプロパティがあります。Windows Forms や XAMLアプリケーション(Sliverlight, WPF)には、ロケーターとして使用できる独自のメタデータがあります。

よいロケーターは以下のような性質を持ちます。

  • 一般的に、ページやビューの階層内における要素の位置には依存しません。つまり、テキストボックスを画面の別の場所に移動したとき、ロケーターが壊れてはいけません。この条件を満たさないものの筆頭がXPathです。XPathは便利なツールとして使える場合もありますが、XPathを使用したロケーターは、ページの階層内で要素が特定の位置にあることに依存しています。ID、Nameなどの属性/プロパティに基づくロケーターを使用するほうがよいでしょう。
  • ロケーターは、動的に生成される属性やプロパティに依存するべきではありません。IDやその他の共通属性を動的に生成することで悪名高いフレームワークやUIコンポーネントも存在します。このような場合、開発者が制御または変更可能な属性を使用するのがよいでしょう。多くの場合、開発者がUIテクノロジーやスタックの動作を変更し、特定の要素固有のカスタマイズされたメタデータに基づいてロケーターを作成するなど、ロケーターをより安定させることが可能です。具体的な例としては、グリッドの行にパート番号や順序IDを含めるなどです。

壊れにくいロケーターを作成できるよう、システムのUIおよび利用する表示フレームワークを最大限に活用する方法を知ることは、価値が高くメンテナンスコストの低いUIテストを作成するうえで非常に大きな一歩です。

3.ページオブジェクトパターンを利用する

優れたロケーター戦略の理解につづいて、それに関連する重要概念、つまりPage Object Pattern (POP)の利用が挙げられます。ページオブジェクトは、先に述べたDRY (Don’t Repeat Yourself) 原則から生まれたものです。質の低いUIテストスイートでは、複数のテストファイルのあちこちにロケーターが散在しているため、テスト対象ページに何らかの変更があった場合、非常に面倒なことになります。

ページオブジェクトとは、ページに関連するロケーターと振る舞いを特定の1つのクラスにカプセル化するという発想です。ページオブジェクトを利用すると、UIが変更されたときも(これは仮定ではなく必然です)、通常はテストの変更が1箇所で済みます。

さらに、適切にページオブジェクトを利用すると、ページやビューの詳細な振る舞いを抽象化あるいは隠蔽できるため、テストスクリプトでは、表示テクノロジーの詳細を扱う方法ではなく、もっと大きなユースケースに集中できます。前に挙げたコンタクト先グリッド用の簡単なページオブジェクトのサンプルは次のとおりです。

public class ContactGridPageObject
{
    public static string GRID_ID = "grid";
    public static string CREATE_BTN_ID = "create_btn";
    public IWebElement CreateButton;
    private IWebDriver browser;
    public ContactGridPageObject (IWebDriver browser)
    {
        this.browser = browser;
        WebDriverWait wait = new WebDriverWait(browser, TimeSpan.FromSeconds(30));
        wait.Until(ExpectedConditions.ElementExists(By.Id(CREATE_BTN_ID)));
        CreateButton = browser.FindElement(By.Id(CREATE_BTN_ID));
    }

    public IWebElement GetContactGrid()
    {
        return browser.FindElement(By.Id(GRID_ID));
    }

    public IWebElement GetCreateButton()
    {
        return browser.FindElement(By.Id(CREATE_BTN_ID));
    }

    public ContactPopUpPageObject GetContactPopUp()
    {
        CreateButton.Click();
        ContactPopUpPageObject popup =
        new ContactPopUpPageObject(browser);
        return popup;
    }

    public IWebElement GetGridRowById(string rowId)
    {
        return browser.FindElement(By.Id(rowId));
    }

    /*
    * Grid as currently configured appends contact's
    * LName to the dynamically generated ID,
    * in the form of
    * 48-Holmes
    * Ergo, we can use the CSS selector id$="Holmes" to match.
    */
    public IWebElement GetGridRowByIdSubstringContactName( string contactName)
    {
        string selector = "tr[id$='" + contactName + "']";
        return browser.FindElement(By.CssSelector(selector));
    }

ページ自体についてはそれほど理解していなくても、このページオブジェクトを利用すると、よりクリーンなテストを作成できることはおわかりになるでしょう。テストスクリプトは単にグリッド上のボタンをクリックしたり、コンタクト先の名前に基づいて行を取得するよう要求するだけです。テストではページ上の任意のアイテムにどのようにアクセスするかという詳細を知る必要はありません。単にさまざまなサービス(グリッドの取得、コンタクト先の取得)やアクション(ボタンをクリックするなど)を提供するものとしてページを扱うことができます。

ページオブジェクトがもたらす利点は、どのようなUIテスト自動化テストスイートにとっても、最も重要なものの1つです。

4.非同期アクションの処理方法を知る

非同期アクションとは、アクションの完了を待つあいだシステムがブロックまたはロックされることがないアクションを指します。非同期アクションが実行される場合、テストスクリプトがアクション全体の完了を待たずに次のアクションに進んでしまうことがあります。たとえば、Webページのメニューを使用して検索結果をフィルタリングする場合を考えてみましょう。システムは、別のプロセスまたはスレッドが検索結果リストを更新しているあいだも処理を進めることができます。結果として、テストスクリプトはリストがただちにフィルタリングされることを期待しているにもかかわらず、実際はわずかな遅延があるために、テストが失敗します。

非同期アクションは、UIテスターが対処するべき問題の中でも最も厄介なものです。経験豊富なテスターでさえも、非同期アクションの対処には試行錯誤する場合があります。

ふたたびコンタクト先グリッドの例で説明すると、[Create new contact]ボタンをクリックすると、システムはポップアップダイアログを表示します。そのとき、ボタンのクリックから実際にダイアログが表示され、ユーザーの入力を受け付ける状態になるまでのあいだにミリ秒単位のわずかな遅延が発生します。このわずかな遅延は、人間にはほぼ認識できませんが、自動化スクリプトはまったく中断しないため、大きな問題を引き起こします。操作するべきポップアップを見つけられないため、おそらくスクリプトは失敗するでしょう。

ありがたいことに、ある程度の機能を備えた最近のUIテスト自動化フレームワークまたはツールであれば、安定的に信頼性のある方法で非同期アクションの問題を解決する機能を提供しています。解決策の根底は非常に単純です。スクリプトが次のステップに進むために必要な条件が整うのを待ってから次のステップに進めばよいのです。こう言うと、たいしたことではないように聞こえるかもしれませんが、そうではありません。

このコンタクト先の例では、[Create new contact]ボタンをクリックしてから、ダイアログ上のテキストフィールドが存在し、入力を受け付ける状態になるのを待つ必要があります。Selenium WebDriverのC#バインディングを使用した場合のコードの一部は次のとおりです。WebDriverでは、WebDriverWaitクラスおよびExpectedConditionsメカニズムを使用して、非同期アクションを柔軟に、かつ安定した方法で処理することができます。

wait = new WebDriverWait(browser, new TimeSpan(30000000));
wait.Until(ExpectedConditions.ElementExists(By.Id("create_btn")));
browser.FindElement(By.Id("create_btn")).Click();
wait.Until(ExpectedConditions.ElementExists(By.Name("Region")));
browser.FindElement(By.Name("Region")).SendKeys("Bellvue");
browser.FindElement(By.Name("Company")).SendKeys("Companions United");
browser.FindElement(By.Name("LName")).SendKeys("Whitehall");
browser.FindElement(By.Name("FName")).SendKeys("Raj");
browser.FindElement(By.ClassName("k-grid-update")).Click();

スクリプトは、まずCreateボタンが存在するのを待ってからクリックしていることに注意してください。その後、Regionフィールドが表示されるのを待ってから、さまざまなフィールドに入力し、最後にダイアログを送信しています。

この、「必要なものを待ってから使用する」パターンで、たいていのチームが遭遇する非同期処理の問題の90%を解決できます。

5.Thread.Sleepを使用しない

5番目のアドバイスは4番目と同じ領域にあり、密接に関連しています。つまり、適切な非同期アクションの処理です。このアドバイスの背景にある問題は、非常によくあるもので、しかも非常に厄介であるため、独立した項目として位置づけるに価します。テストで決してThread.Sleep() を使用しないようにしましょう。

Thread.Sleep() または使用しているプラットフォームでそれに相当するものは、非同期処理に関連して発生したりしなかったりするテストの失敗に対する解決策としては、非常にまずいものです。とりあえず固定の遅延時間をはさんで先へ進むという対応をしたくなるでしょうが、そのような遅延が積み重なると、自動テストスイートの実行時間が何分、ときには何時間も増えることになります。

成熟したテストフレームワークであれば、前に示した例の WebDriverWait に相当する機能が必ずあります。このような待機メカニズムは、ポーリングとタイムアウトのチェックによって非同期アクションを処理します。待機メカニズムを使用すると、非同期アクションの完了を待機できるいっぽう、タイムアウトがあるため、同期アクションが完了しないか失敗したときに無限ループに陥ってテストがスタックするのを防ぐことができます。大雑把に言えば、待機メカニズムはwhile-doループに相当します。ボタンがクリックできる状態になるのを待つ条件を考えてみてください。

  1. タイムアウト期限を超過しないあいだ、以下を繰り返す
  2. ボタンが存在し、クリックできる状態であるかをチェックする。条件を満たさない場合、ステップ1に戻る

このモデルでは、テストは非同期アクションが完了するのに必要な時間だけ待機します。時間の浪費はありません。

いっぽう、Thread.Sleep()やPause()、あるいは他の言語プラットフォームでこれらに相当するものでは、固定された遅延時間があります。経験の少ないテスターは、デフォルト値の30秒を使用することがよくあります。これは、非同期アクションがいつ完了したかにかかわらず、テストスクリプトが常に30秒中断することを意味します。

上記の例では、ボタンが表示されクリック可能になるまでに5ミリ秒しかかからなくても、テストが常に30秒間ストップすることになります。

このような固定された遅延が大規模なテストスイートのあちこちに散らばっていると想像してみてください。遅延がたちまち何分にもなることがわかるでしょう。ログ記録などのよく使われるアクションに遅延が含まれている場合、テストスイートの実行時間が何時間も増える可能性があります。

Thread.Sleep() 等は、スクリプトで発生したりしなかったりするタイミングの問題の原因を調べるときには便利な方法ですが、本番のテストスイートに残すべきケースはほとんどありません。経験豊富なチームであれば、何千ものテストがあるテストスイートの中にThread.Sleep() 等は10個もないでしょう。

クリーンで柔軟、実行が速いテストスイートをキープする

質の高い自動テストスイートを作成するには、質の高いシステムコードを作成するときと同じ努力が必要です。注意深く計画し、価値を意識し、よく考えてスキルとプラクティスを適用する必要があります。

ここで紹介した5つのステップに従うと、面倒なメンテナンス作業に煩わされることなく、テストスイートがスムーズに実行される状態を保つことができます。

JimはPillar Technologyのエグゼクティブコンサルタントとして、さまざまな組織のソフトウェアデリバリープロセスの改善を支援しています。Guidepost Systems のオーナー/運営者でもあり、課題に取り組んでいる組織と直接的に関わっています。1982年にアメリカ空軍に入職して以来、IT業界のさまざまな領域での経験があります。長年にわたってチームや顧客がすばらしいシステムをリリースできるよう支援してきたほか、LAN/WANおよびサーバー管理業務にも携わりました。新興企業からFortune 10企業まで、幅広い組織に協力し、デリバリープロセスを改善して顧客によりよい価値を届けられるようサポートしてきました。プライベートでは、キッチンでワイングラスを手にしていたり、Xboxをプレイしたり、家族とハイキングを楽しんだり、ギターを練習しようとしてガレージに追いやられたりしています。

(この記事は、開発元Gurock社の Blog 「Five Things to Make Your User Interface Tests Better: Bad Practices Cost You Time, Money, and Tears」2019年2月17日の翻訳記事です。)

eBook 公開中

Paul Gerrard著 効果的なテスト管理12の秘密 (日本語)

テスト計画やテスト管理に役立つ12のトピックを解説します。

詳細はこちら