Seleniumを適用してWebテストを行う

10334 ワード

Seleniumを使用してWebテストを行うと、次のような問題が発生することがよくあります.
  • name、id、xpathなどのページ要素を大量に使用します.機能の変更、UIの再構築、インタラクティブな改善は、これらの要素に影響し、Seleniumテストが非常に脆弱になります.
  • あまりにも詳細なページ操作は行動の意図を体現しにくく、しばらくするとテストの本来の目的を本当に把握することが難しくなり、Seleniumテストのメンテナンスが難しくなります.
  • 特定のデータの値の取得に依存し、個別のデータが正当でない場合、テストは失敗するが、このような失敗は機能の欠如を識別することができず、Seleniumテストが脆弱になり、メンテナンスが困難になる.

  • これらの点から直接派生した結果は、新しいテストを絶えず追加し、既存のテストを再構築し、利用することは極めて少ない.実はこれも普通で、ユニットテストのテストが書きすぎて、このような問題もあります.ただし、Seleniumの実行速度は遅く(相対ユニットテスト)、テストが増えるにつれて、実行時間は耐えられないほど増加します.意図不明でメンテナンスが困難なSeleniumテストのセットで、構築(Build)のたびに40分か2時間も殺すことができ、2時間もパソコンの前に座って450個のSeleniumテストが通過するのを待っていた悲惨な経験がありました.そのため、Seleniumテストを合理的かつ効果的に計画することは、特に切実で重要です.現在の比較的有効な方法は、大きく言えば、分野ベースのWebテスト(Domain Based Web Testing)と呼ぶことができ、具体的にはPage Object Patternである.
    Page Object Patternには、Driver、Page、Navigator、Shortcutの4つの基本概念があります.Driverは、Selenium、例えばWatir、例えばHttpUnitなどの真の実装メカニズムをテストします.それらはどのように本当に1つのWeb行為を実行するかを知っていて、通常Click、Select、Typeなどのような具体的な行為を表す方法を含む.Pageは、id、name、class、xpathなどの実装の詳細を理解し、ユーザーがどのような操作を行うことができるかを記述するページの構造を理解する特定のページのパッケージです.NavigatorはURLを代表し、ページ操作を経ない直接ジャンプを表す.最後にShortcutはhelperメソッドで、具体的なニーズに応じて決める必要があります.次に、ログインページをテストするという超簡単な例を見てみましょう.

    1. Page Object


    個別のログインページを使用してログインすると、ログイン操作がLoginPageというpage objectにカプセル化される可能性があります.
    class LoginPage
      def initialize driver
        @driver = driver
      end
      
      def login_as user
        @driver.type 'id=...', user[:name]
        @driver.type 'xpath=...', user[:password]
        @driver.click 'name=...'
        @driver.wait_for_page_to_load
      end
    end

    login_asはビジネスの意味を持つページ動作です.login_Asメソッドでは,page objectがid,xpath,nameなどの情報によるログイン操作を担当する.テストでは、このpage objectを使用することができます.
    page = LoginPage.new $selenium
    page.login_as :name => 'xxx', :password => 'xxx'

    しかし、Rubyを使用する以上、ruby sugarをいくつか使用しましょう.onメソッドを定義してページ操作を表現する環境を定義します.
    def on page_type, &block
      page = page_type.new $selenium
      page.instance_eval &block if block_given?
    end

    その後、page objectのクラス名定数とblock記述を使用して、特定のページで操作できます.
    on LoginPage do
      login_as :name => 'xxx', :password => 'xxx'
    end

    行動方法に加えて、登録ページのウェルカムワードを取得する方法など、page objectでページ情報を取得する方法を定義する必要があります.
    def welcome_message
      @driver.get_text 'xpath=...'
    end

    このようなテストは、より生き生きと表現することができます.
    on LoginPage do
      assert_equal 'Welcome!', welcome_message
      login_as :name => 'xxx', :password => 'xxx'
    end

    すべてのページをPage Objectでカプセル化すると、テストとページ構造の結合が効果的に分離されます.テストではlogin_などのasとadd_product_to_cartのようなビジネス行為は、id、nameなどの具体的で変化しやすいページ要素に頼る必要はありません.これらのページ要素が変化すると、対応するpage objectを変更するだけでよいが、既存のテストではあまり変更する必要はない.

    2. Assertation


    行動だけがテストにならないので,行動結果を判断し,いくつかの断言を行う必要がある.上の例を簡単に振り返ると、ログインに成功したとどう判断するかという重要な問題が解決されていないことがわかります.本当にログインページにあることをどうやって知ることができますか?次のコードを呼び出すとどうなりますか?
    $selenium.open url_of_any_page_but_not_login
    on LoginPage {...}

    そのため、page objectにいくつかの断言的な方法を追加する必要があります.少なくとも、各ページには、本当にこのページに達したかどうかを判断する方法があるはずです.このページにいなければ、ビジネス行為はできません.次の方法でLoginPageを変更します.
    LoginPage.class_eval do
      include Test::Unit::Asseration
      def visible?
        @driver.is_text_present(...) && @driver.get_location == ...
      end
    end

    visibleで?方法では、URLアドレス、特定のUI構造、または要素などの特定のページ要素を判断することによって、実際にページ上に存在するかどうかを判断することができる.私たちが現在表現しているテストの基本構造はonメソッドによって完成されています.私たちはonメソッドに断言を追加して、本当にあるページにあるかどうかを判断し、このページにいなければビジネス操作を行いません.
    def on page_type, &block
      page = page_type.new $selenium
      assert page.visible?, "not on #{page_type}"
      page.instance_eval &block if block_given?
      page
    end

    この方法は神秘的にpageオブジェクトに戻り、ここでは比較的巧みなテクニックです.実際、私たちはpageを利用したいだけです!=nilという事実は、ページのフローを断言します.例えば、次のコードは、ログインに成功したページのフロープロセスを説明します.
    on LoginPage do
      assert_equal 'Welcome!', welcome_message
      login_as :name => 'xxx', :password => 'xxx'
    end
    assert on WelcomeRegisteredUserPage

    この基本的な断言に加えて、カートページでカートが空いているかどうかを判断する断言を定義することができます.
    def cart_empty?
      @driver.get_text('xpath=...') == 'Shopping Cart(0)'
    end

    注意しなければならないのは、page objectにTest::Unit::Assertionモジュールを導入したが、断言方法にassert*メソッドは使用されていないことだ.なぜなら、概念的にはpage objectはテストではないからです.いくつかの本当の断言を含んで、1つは概念が混乱して、2つはpage objectをいくつかのシーンに対するtest helperに変えやすくて、後でテストするメンテナンスに不利で、そのため私達は往々にして断言方法を1つの普通の戻り値がbooleanの方法に実現する傾向があります.

    3. Test Data


    テスト意図の表現は、動作の説明だけでなく、次の2つのコードのようなテストデータもあります.
    on LoginPage do
      login_as :name => 'userA', :password => 'password'
    end
    assert on WelcomeRegisteredUserPage
    
    registered_user = {:name => 'userA', :password => 'password'}
    on LoginPage do
      login_as registered_user
    end
    assert on WelcomeRegisteredUserPage

    テストは同じものですが、2番目のテストは、登録されたユーザーを使用してログインし、ウェルカムページにアクセスする必要があるというテストの意図をよりよく示しています.このテストを見ると、ユーザー名やパスワードが具体的に何なのか、どのようなテストケースを表現しているのかに関心がありません.これはDataFixtureで実現できます.
    module DataFixture
      USER_A = {:name => 'userA', :password => 'password'}
      USER_B = {:name => 'userB', :password => 'password'}
    
      def get_user identifier
        case identifier
        when :registered then return USER_A
        when :not_registered then return USER_B
        end
      end
    end

    ここでは、ユーザーAは登録済みのユーザーであり、ユーザーBは登録されていないユーザーであるというテストケースと具体的なデータに対応しています.ある日、ログインユーザー名をメールボックスに変更する必要がある場合は、対応するテストを変更することなく、DataFixtureモジュールを変更するだけでいいです.
    include DataFixtureDat
    
    user = get_user :registered
    on LoginPage do
      login_as user
    end
    assert on WelcomeRegisteredUserPage

    もちろん、より複雑なテストでは、DataFixtureは実際のデータベースまたはRails Fixtureを使用してこのような対応を完了することができますが、全体的な目的は、テストとテストデータの有効性の結合を分離することです.
    def get_user identifier
      case identifier
      when :registered then return User.find '....'  
      end
    end

    4.Navigator


    インタフェース要素と同様に、URLも変化しやすく、意図を表現しにくい要素であるため、Navigatorを使用してテストとデカップリングすることができます.具体的な方法はTest Dataと似ていますが、ここでは説明しません.次の例を示します.
    navigate_to detail_page_for @product
    on ProductDetailPage do
      ....
    end

    5. Shortcut


    前に私たちはすでに良い基礎を持っていて、Seleniumテストと様々な脆弱で意図不明な要素を分離しました.では、最後にshortcutはケーキの上で最もきれいなクリームにすぎません.きれいな文法を持つhelperを定義します.
    def should_login_successfully user
      on LoginPage do
        assert_equal 'Welcome!', welcome_message
        login_as user
      end
      assert on WelcomeRegisteredUserPage
    end

    次に別のmagicメソッドがあります.
    def given identifer
      words = identifier.to_s.split '_'
      eval "get_#{words.last} :#{words[0..-2].join '_'}"
    end

    以前のテストは次のように書き換えることができます.
    def test_should_xxxx
      should_login_successfully given :registered_user
    end

    これは結論的なshortcut記述であり、よりbehaviourの書き方もできます.
    def login_on page_type
      on page_type do
        assert_equal 'Welcome!', welcome_message
        login_as @user
      end 
    end
    
    def login_successfully
      on WelcomeRegisteredUserPage
    end
    
    def given identifer
      words = identifier.to_s.split '_'
      eval "@#{words.last} = get_#{words.last} :#{words[0..-2].join '_'}"
    end

    最後に、テストは検収条件のようなものになります.
    def test_should_xxx
      given :registered_user
      login_on LoginPage
      assert login_successfully
    end

    とにかくショットカットは良し悪しに関係なく、想像力だけにかかわるもので、ルビーDSLを存分に振りまいましょう!

    結論


    Seleniumは人を愛し憎むもので、Seleniumを誤って使用すると、敏捷なチーム全体の開発リズムに災難的な影響を与える.しかし、Seleniumを正しく使用する原則もかなり簡単です.
  • 脆弱で変化しやすいページ要素とテストを分離することによって、ページの変化がテストに大きな影響を及ぼさないようにします.
  • は、テストデータの意図を明確に指定し、テスト用に具体的なデータを使用しない.
  • 可能な限り、テストの意図を明確に表現し、テストを理解しやすくする.

  • もちろん、これらの基本原則に従う以外に、page objectや他のdomain based web testing技術を使用するのは良い選択です.これらは、Seleniumテストの規模をより容易に制御し、カバー率と実行効率をよりよくバランスさせ、高品質のWebプロジェクトをより効果的に提供するのに役立ちます.
    この文書には、私がここ数週間、Seleniumテストを再構築したときに採用した実際の技術について説明しています.Nick DrewがDriver、Page、Nagivator、Shortcutの階層関係を明確に区別してくれたことに感謝します.それらは私の実践全体の礎を構成しています.Chris Leishmanに感謝して、彼とペアを組んでプログラミングする過程で、彼は私にRuby DSLを鍛えてくれた.またMark RyallとAbhiは、プロジェクトにTest Data Fixtureを導入したのは初めてで、すべての人の仕事が簡単になりました.
    作者紹介:
    徐昊、ThoughtWorksコンサルタントと敏捷な過程監督.
    BJUG
    (Beijing Java User Group)と
    AgileChina
    主な創始者の一人.RSSer(Ruby,Smalltalk & Scheme).現在,主にコンパイル理論の研究とDSL(Domain Specified Language)の実際のプロジェクトにおける応用の普及に力を入れている.彼のブログのアドレスは:
    http://www.blogjava.net/raimundox
    .