JMeter WebDriver Plugin を使ってみた

はじめに

http://jmeter.apache.org/index.html

Apache JMeterは主にWebアプリの負荷テストで使用されるツールです。
基本的にはシナリオにしたがってリクエストを投げて、想定どおりのレスポンスが戻るまでの時間を測定し、集計するというツールですが、ユーザーのインタラクティブな操作に対応したテストを行うのは苦手です。例えば、「ユーザーがWebアプリ上である操作を行った時、裏で多数の非同期通信が行われた結果がUI上に反映されるまでの時間を測定する」といったシナリオを書くのは結構面倒なのです。

そこで、JMeterのシナリオ上からブラウザを Selenium WebDriverを通じて呼び出してしまえというWebDriverプラグインの出番になります。

jmeter-plugins.org

JMeterシナリオ上でブラウザへの操作を記述し、リクエストはJMeterではなくあくまでブラウザから行い、レスポンス(とDOMへの反映)もブラウザが受け取ります。ブラウザを起動する必要が有るため、例えば大規模ユーザの同時アクセスを想定しているような、JMeterが得意な負荷テストには向きません。しかし、ブラウザを使用することで、ユーザの操作に近い形でのテストシナリオを記述する事が可能になります。

jMeterとWebDriver APIそれぞれについてある程度知識のある人向けに、JMeter WebDriver Plugin を使って Mozilla Firefox によるテストシナリオを作成してみます。

インストール方法

1. plugins-managerの導入

jmeter-plugins.org

から plugins-managerのjarファイルを入手し、以下のフォルダへコピーします。

${JMeterインストールフォルダ}/lib/ext

現時点ではjmeter-plugins-manager-0.10.jarです。

2. WebDriver Pluginのインストール

plugins-managerの導入後にJMeterを起動すると、メニューの [オプション]配下に[Plugins Manager]が追加されるので、クリックします。
Plugins Mnagerダイアログが表示されるので、[Available Plugins]タブを選択し、インストール候補プラグイン一覧から[Selenium WebDriver Support]にチェックを入れ、「Apply Changes and Restart JMeter」ボタンをクリックします

現時点では

${JMeterインストールフォルダ}/lib/ext

jmeter-plugins-webdriver-1.4.0.jarがダウンロードされ、さらにlibフォルダに関連jarファイル群がダウンロードされます。

3. selenium-firefox-driverのjarファイルを更新

jmeter-plugins-manager v1.10, jmeter-plugins-webdriver v1.4.0の組み合わせでは以下の手順が必要です)

いったんJMeterを停止した後、

${JMeterインストールフォルダ}/lib

フォルダに"selenium-firefox-driver-2.xx.x.jar"(xは任意のバージョン番号)が存在する事を確認します。そして

Maven Repository: org.seleniumhq.selenium » selenium-firefox-driver


にv2.xx.x系のより新しいバージョンのjarファイルが存在するようであれば、jarファイルを入手して入れ替えます。Selenium WebDriverは、ブラウザが更新されると動かなくなる場合が多々有ります。そのつど上記jarをより新しいバージョンに入れ替えてください。

4. firefox 停止時のエラーを対策

jmeter-plugins-manager v1.10, jmeter-plugins-webdriver v1.4.0の組み合わせでは以下の手順が必要です)

ここまでJMeterをセットアップした状態で、WebDriver Pluginを使ってFirefoxを起動すると、シナリオ終了時に以下のような例外が出力されます。

2016/09/12 20:47:28 ERROR - jmeter.JMeter: Uncaught exception:  java.lang.NoClassDefFoundError: com/sun/jna/platform/win32/Kernel32
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)

この問題に対応するため、

${JMeterインストールフォルダ}/lib

へplatform-3.5.1.jar およびjna-4.2.1.jarを導入し、JMeterを再起動します。
詳細は以下のサイトを参照。

hellotestworld.com

JMeterシナリオの作成

ここまでセットアップした後、「設定エレメント」として各ブラウザに対応するWebDriver設定が追加されます。この設定エレメントをシナリオへ加えることで、以降はWebDriver Pluginでは該当ブラウザが使用されるようになります。

f:id:ka-ka_xyz:20160913001813p:plain


また、「サンプラー」としてWebDriver Samplerが追加されます。

f:id:ka-ka_xyz:20160913001825p:plain

このSamplerにはスクリプトが記述できます。デフォルトではjavascript

WDS.sampleResult.sampleStart()
WDS.browser.get('http://jmeter-plugins.org')
WDS.sampleResult.sampleEnd()

のようなテンプレが入力されています。
このテンプレでは、測定開始後にjmeter-plugins.orgへアクセスし、測定を完了するという処理を行っています。

新規にシナリオを作成し、スレッドグループ配下にWebDriver設定、WebDriverサンプラー、リスナー「結果を表で表示」を追加してシナリオを保存します。この状態でシナリオを実行するとブラウザが立ち上がり、jmeter-plugins.orgへアクセスした後にブラウザウィンドウが閉じられます。実行結果はリスナーに出力されます。


WebDriverサンプラーの詳細については以下を参照。

www.blazemeter.com
www.blazemeter.com

複数のWebDriverサンプラー間でコードを再利用

各WebDriverサンプラーjavascript(あるいは選択可能な他のスクリプト)でコードを書くことが出来ますが、変数のスコープは各サンプラーの中に限定されます。ただ、それでは不便なので、サンプラー間でコードを共有してみます*1

シナリオに「設定エレメント」-「ユーザー定義変数」を追加し、ユーザー定義変数"lib"を定義します。
そして、"lib"を選択した状態で"Detail"ボタンをクリックすると、ユーザー定義変数を複数行編集出来るようになります。

サンプルとして、以下の内容を入力します。

var mylib = mylib || {};

mylib.pkg = JavaImporter(org.openqa.selenium);
mylib.conditions = org.openqa.selenium.support.ui.ExpectedConditions;
mylib.wait = new org.openqa.selenium.support.ui.WebDriverWait(WDS.browser, 10);
mylib.vars = org.apache.jmeter.threads.JMeterContextService.getContext().getVariables();

mylib.google = (function() {
  var googleUrl = 'https://www.google.co.jp';

  var access = function() {
    WDS.browser.get(googleUrl);
  };

  var search = function(keyword) {
  	var inputQuery = 'input#lst-ib';
  	mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(inputQuery)));
    var inputElem = WDS.browser.findElement(mylib.pkg.By.cssSelector(inputQuery));
    inputElem.sendKeys(mylib.pkg.Keys.chord(mylib.pkg.Keys.CONTROL, 'a'), keyword, mylib.pkg.Keys.ENTER);
  };

  var waitForResultsShown = function() {
    var resultListQuery = '#search';
    mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(resultListQuery)));
  };

  return {
    access: access,
    search: search,
    waitForResultsShown: waitForResultsShown
  };
})();

名前空間 mylib に、googleの検索フォームへの入力を行う関数やユーテリティ類を定義しています。
そして、サンプラー側で以下のスクリプトを記述します。


サンプラー1

//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get('lib'));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search('jMeter');
mylib.google.waitForResultsShown();


WDS.sampleResult.sampleEnd();

サンプラー2

//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get('lib'));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search('Selenium WebDriver');
mylib.google.waitForResultsShown();

WDS.sampleResult.sampleEnd();

ユーザー定義変数"lib"を文字列として読み出し、evalすることで変数"lib"中に書かれた"mylib"定義が読み出されます。

eval を使っている事に嫌悪感があるかもしれませんが、JMeter WebDriver Pluginの制約上しょうが無いかなと。"Eval is Evil"は原則として正しいとは思いますが、今回は外部から入力された文字列をevalしている訳ではなく、あくまでJMeterのシナリオで変数として定義された文字列をしようしているので、危険は無いはず(おねがいみのがして)。

f:id:ka-ka_xyz:20160913004600p:plain

このシナリオを実行すると

1. google.co.jpへアクセス
2. キーワード"jMeter"で検索
3. 検索結果が表示されるまで待つ
4. google.co.jpへアクセス
5. キーワード"Selenium WebDriver"で検索
6. 検索結果が表示されるまで待つ
7. ブラウザを閉じる

というシナリオが実行されます。


jmxファイルの内容は以下のとおり

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="2.9" jmeter="3.0 r1743807">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="テスト計画" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="スレッドグループ" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="ループコントローラ" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <longProp name="ThreadGroup.start_time">1473680458000</longProp>
        <longProp name="ThreadGroup.end_time">1473680458000</longProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig guiclass="com.googlecode.jmeter.plugins.webdriver.config.gui.FirefoxDriverConfigGui" testclass="com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig" testname="jp@gc - Firefox Driver Config" enabled="true">
          <stringProp name="WebDriverConfig.proxy_type">SYSTEM</stringProp>
          <stringProp name="WebDriverConfig.proxy_pac_url"></stringProp>
          <stringProp name="WebDriverConfig.http_host"></stringProp>
          <intProp name="WebDriverConfig.http_port">8080</intProp>
          <boolProp name="WebDriverConfig.use_http_for_all_protocols">true</boolProp>
          <stringProp name="WebDriverConfig.https_host"></stringProp>
          <intProp name="WebDriverConfig.https_port">8080</intProp>
          <stringProp name="WebDriverConfig.ftp_host"></stringProp>
          <intProp name="WebDriverConfig.ftp_port">8080</intProp>
          <stringProp name="WebDriverConfig.socks_host"></stringProp>
          <intProp name="WebDriverConfig.socks_port">8080</intProp>
          <stringProp name="WebDriverConfig.no_proxy">localhost</stringProp>
          <boolProp name="WebDriverConfig.maximize_browser">true</boolProp>
          <boolProp name="WebDriverConfig.reset_per_iteration">false</boolProp>
          <boolProp name="WebDriverConfig.dev_mode">false</boolProp>
          <boolProp name="FirefoxDriverConfig.general.useragent.override.enabled">false</boolProp>
          <boolProp name="FirefoxDriverConfig.network.negotiate-auth.allow-insecure-ntlm-v1">false</boolProp>
          <collectionProp name="FirefoxDriverConfig.general.extensions"/>
          <collectionProp name="FirefoxDriverConfig.general.preferences"/>
        </com.googlecode.jmeter.plugins.webdriver.config.FirefoxDriverConfig>
        <hashTree/>
        <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="ユーザー定義変数" enabled="true">
          <collectionProp name="Arguments.arguments">
            <elementProp name="lib" elementType="Argument">
              <stringProp name="Argument.name">lib</stringProp>
              <stringProp name="Argument.value">var mylib = mylib || {};

mylib.pkg = JavaImporter(org.openqa.selenium);
mylib.conditions = org.openqa.selenium.support.ui.ExpectedConditions;
mylib.wait = new org.openqa.selenium.support.ui.WebDriverWait(WDS.browser, 10);
mylib.vars = org.apache.jmeter.threads.JMeterContextService.getContext().getVariables();

mylib.google = (function() {
  var googleUrl = &apos;https://www.google.co.jp&apos;;

  var access = function() {
    WDS.browser.get(googleUrl);
  };

  var search = function(keyword) {
  	var inputQuery = &apos;input#lst-ib&apos;;
  	mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(inputQuery)));
    var inputElem = WDS.browser.findElement(mylib.pkg.By.cssSelector(inputQuery));
    inputElem.sendKeys(mylib.pkg.Keys.chord(mylib.pkg.Keys.CONTROL, &apos;a&apos;), keyword, mylib.pkg.Keys.ENTER);
  };

  var waitForResultsShown = function() {
    var resultListQuery = &apos;#search&apos;;
    mylib.wait.until(mylib.conditions.presenceOfElementLocated(mylib.pkg.By.cssSelector(resultListQuery)));
  };

  return {
    access: access,
    search: search,
    waitForResultsShown: waitForResultsShown
  };
})();
</stringProp>
              <stringProp name="Argument.metadata">=</stringProp>
            </elementProp>
          </collectionProp>
        </Arguments>
        <hashTree/>
        <com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler guiclass="com.googlecode.jmeter.plugins.webdriver.sampler.gui.WebDriverSamplerGui" testclass="com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler" testname="WebDriver Sampler1" enabled="true">
          <stringProp name="WebDriverSampler.script">//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get(&apos;lib&apos;));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search(&apos;jMeter&apos;);
mylib.google.waitForResultsShown();


WDS.sampleResult.sampleEnd();</stringProp>
          <stringProp name="WebDriverSampler.parameters"></stringProp>
          <stringProp name="WebDriverSampler.language">javascript</stringProp>
        </com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler>
        <hashTree/>
        <com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler guiclass="com.googlecode.jmeter.plugins.webdriver.sampler.gui.WebDriverSamplerGui" testclass="com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler" testname="WebDriver Sampler2" enabled="true">
          <stringProp name="WebDriverSampler.script">//load mylib
eval(org.apache.jmeter.threads.JMeterContextService.getContext().getVariables().get(&apos;lib&apos;));

WDS.sampleResult.sampleStart();
mylib.google.access();
mylib.google.search(&apos;Selenium WebDriver&apos;);
mylib.google.waitForResultsShown();

WDS.sampleResult.sampleEnd();
</stringProp>
          <stringProp name="WebDriverSampler.parameters"></stringProp>
          <stringProp name="WebDriverSampler.language">javascript</stringProp>
        </com.googlecode.jmeter.plugins.webdriver.sampler.WebDriverSampler>
        <hashTree/>
        <ResultCollector guiclass="TableVisualizer" testclass="ResultCollector" testname="結果を表で表示" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

*1:ただし、今回紹介する方法だと共有できるのはあくまでコードだけで、状態は共有されないので注意