Taurusで手軽に初めての性能テスト


## Taurusとは

TaurusはBlazeMeterが公開しているオープンソースのテスト自動化フレームワークです。
JMeter以外にもGatlingやLocustなど様々な負荷試験ツールのラッパーとして動かせることや、レポーティング機能が充実しているところが特徴です。

https://gettaurus.org/


## Taurusの登場背景は？

複数のテストツールを組み合わせたテスト自動化において、ツールのバージョン管理やテスト環境の整備、テストシナリオのパラメータ化などの課題があったことが挙げられます。

従来、負荷テストにはJMeter、機能テストにはSelenium、統合テストにはPythonなどのテストツールが使用されることが多く、それぞれが異なるスクリプトファイルで実行されていました。しかし、テストが複雑になると、テストスクリプトの管理や統合が困難になり、テストの再利用性や保守性が低下する問題が生じました。

Taurusは、このような問題を解決するために開発されたツールで、複数のテストツールを統合し、テストスクリプトの管理や統合を自動化することで、テスト自動化の効率性や品質を向上させることを目的としています。

### ここがスゴい

とてもまとまっているので、コチラを参照ください。Taurusスゴい。

https://engineer.retty.me/entry/2022/12/09/123459

https://zenn.dev/tonchan1216/articles/11afd147ea3dd2734315

## Taurusのユースケースは？

①Taurus単体で利用
Taurus単体でもテストを記述ができます。シンプルで使いやすい。
本記事では、こちらを紹介します

②TaurusからJMeter、seleniumのコードを呼び出して利用
JMeterを使用して、多数のリクエストを送信してウェブアプリケーションの性能を評価し、Seleniumを使用して、ユーザーが実際に操作するように複数のテスト実行を並行してキックすることができます。これにより、実際のユーザーが行うようなアクションに焦点を当てた性能テストを実行することができます。

またTaurusはレポート機能が充実しており見やすいため、単純にJMeterで実行していたものをTaurusから呼び出すだけでも価値があるのではと思います。


``` yaml
execution:
- scenario: sample # Taurusのシナリオ名

scenarios:
  simple:
    script: my-scenario.jmx # JMeterのシナリオファイルを指定

reporting:
- module: console     # 実行中の情報を表示
- module: final-stats # サマリを表示
- module: blazemeter  # オンラインレポートを作成
```

### 参考記事

https://qiita.com/hmsnakr/items/a64507c31d1365dd6bb0

## 複数のテストツールを統合し自動化する具体例

例として以下のような要件を満たすテストを実施する場合を考えます。

- Webアプリケーションの負荷テストと機能テストを行う。
- 複数のシナリオを実行し、テスト結果を統合して分析する。
- テスト実行時のツールのバージョン管理や環境構築を自動化する。

対象は以下のテストサイトを例とします
- https://atgo.rgsis.com/demo/hotel/

## 手順
Taurusを使用して、Apache JMeterとSeleniumを統合したテストスクリプトを作成することができます。

### 1.Taurusのインストールと設定
まず、Taurusをインストールします。手順は以下を参考にしてください。

https://gettaurus.org/docs/Installation/

### 2.テスト環境の設定

次に、Taurusの設定ファイルであるYAMLファイルを作成し、テストシナリオやテスト環境の設定を行います。
以下は、YAMLファイルの例です。

``` yaml

execution:
- concurrency: 10 # 同時実行数を10に設定
  hold-for: 5m    # 負荷をかけ続ける時間を5分に設定
  ramp-up: 2m     # ユーザーの増加を2分間隔で設定

  scenario: yaml_example # 実行するシナリオをyaml_exampleに設定
  
scenarios:
  # yaml_exampleシナリオの設定
  yaml_example:
    retrieve-resources: false # リソースの取得は行わない
    # http://example.com/に対してリクエストを送信
    requests:
      - http://example.com/

reporting:
- module: console     # 実行中の情報を表示
- module: final-stats # サマリを表示
- module: blazemeter  # オンラインレポートを作成


settings:
# 5秒ごとにチェックを実行
  check-interval: 5s
# デフォルトの実行エグゼキュータをJMeterに設定
  default-executor: jmeter

# ローカル環境で実行するように設定
provisioning: local

```

### 2.テストスクリプトの作成
次に、Taurusで使用するJMeterとSeleniumのテストスクリプトを作成します。
JMeterのスクリプトは、JMXファイルで作成し、Seleniumのスクリプトは、Pythonスクリプトとして作成します。

以下は、JMeterのスクリプトの例です。

``` xml
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="3.2">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">10</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <longProp name="ThreadGroup.start_time">1617039010000</longProp>
        <longProp name="ThreadGroup.end_time">1617039010000</longProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Open Landing Page" enabled="true">
          <elementProp name
            ="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments"/>
          </elementProp>
          <stringProp name="HTTPSampler.domain">atgo.rgsis.com</stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
          <stringProp name="HTTPSampler.protocol">https</stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/demo/hotel/</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.monitor">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
          <stringProp name="HTTPSampler.concurrentPool">4</stringProp>
        </HTTPSamplerProxy>
        <hashTree>
          <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Regex Extractor" enabled="true">
            <stringProp name="RegexExtractor.useHeaders">false</stringProp>
            <stringProp name="RegexExtractor.refname">CSRF_Token</stringProp>
            <stringProp name="RegexExtractor.regex">name=&quot;_csrf_token&quot; value=&quot;([^&quot;]+)&quot;</stringProp>
            <stringProp name="RegexExtractor.template">$1$</stringProp>
            <stringProp name="RegexExtractor.default"></stringProp>
            <stringProp name="RegexExtractor.match_number">1</stringProp>
          </RegexExtractor>
          <hashTree/>
        </hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Search Hotel" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="_csrf_token" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">${CSRF_Token}</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="destination" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">Tokyo</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="checkin" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">2022-04-01</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="checkout" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">2022-04-03</stringProp>
                <stringProp name="Argument.metadata">=
</elementProp>
              <elementProp name="rooms_0_adults" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">1</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="rooms_0_children" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">0</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
              <elementProp name="submit" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value"></stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain">atgo.rgsis.com</stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
          <stringProp name="HTTPSampler.protocol">https</stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">/demo/hotel/search</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <boolProp name="HTTPSampler.monitor">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.implementation">HttpClient4</stringProp>
          <stringProp name="HTTPSampler.concurrentPool">4</stringProp>
        </HTTPSamplerProxy>
        <hashTree>
          <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
            <collectionProp name="HeaderManager.headers">
              <elementProp name="Accept" elementType="Header">
                <stringProp name="Header.name">Accept</stringProp>
                <stringProp name="Header.value">text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8</stringProp>
              </elementProp>
              <elementProp name="User-Agent" elementType="Header">
                <stringProp name="Header.name">User-Agent</stringProp>
                <stringProp name="Header.value">Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36</stringProp>
              </elementProp>
            </collectionProp>
          </HeaderManager>
          <hashTree/>
          <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
            <stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
            <stringProp name="Assertion.test_type">2</stringProp>
            <stringProp name="Assertion.custom_message"></stringProp>
            <stringProp name="Assertion.test_strings">Welcome to AGoda Demo Application</stringProp>
            <boolProp name="Assertion.ignore_status">false</boolProp>
            <boolProp name="Assertion.never_assert">false</boolProp>
          </ResponseAssertion>
          <hashTree/>
        </hashTree>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

```
以下は、Seleniumのスクリプトの例です。

``` python
from selenium import webdriver

# ブラウザ起動
driver = webdriver.Chrome()

# ログイン
driver.get("https://atgo.rgsis.com/demo/hotel/")
driver.find_element_by_name("_username").send_keys("demo_user")
driver.find_element_by_name("_password").send_keys("demo_user123")
driver.find_element_by_name("_submit").click()

# ホテル検索
driver.find_element_by_name("destination").send_keys("Tokyo")
driver.find_element_by_name("checkin").send_keys("2022-04-01")
driver.find_element_by_name("checkout").send_keys("2022-04-03")
driver.find_element_by_name("rooms[0][adults]").send_keys("1")
driver.find_element_by_name("rooms[0][children]").send_keys("0")
driver.find_element_by_name("submit").click()

# ブラウザを終了する
driver.quit()


```

### 3.テストシナリオの設定

TaurusのYAMLファイルで、複数のJMXファイルとPythonスクリプトを統合したテストシナリオを設定します。各スクリプトのパラメータや設定値を指定することができます。

以下は、YAMLファイルの例です。

``` yaml
---
execution:
  - scenario:
      hotel:
        variables:
          CSRF_Token: null
          username: demo_user
          password: demo_user123
          destination: Tokyo
          checkin: 2022-04-01
          checkout: 2022-04-03
          adults: 1
          children: 0
      script: hotel_test.yml

scenarios:
  hotel:
    modules:
      jmeter: 'jmeter'
      selenium: 'selenium'
    requests:
      - name: open_landing_page
        jmeter: 'open_landing_page.jmx'
        think-time: 5s
      - name: search_hotel
        jmeter: 'search_hotel.jmx'
        variables:
          CSRF_Token: ${CSRF_Token}
          destination: ${destination}
          checkin: ${checkin}
          checkout: ${checkout}
          adults: ${adults}
          children: ${children}
        selenium-script: 'search_hotel.py'

```


### 4.テストの実行
最後に、Taurusでテストを実行します。
Taurusは、テストシナリオを自動的に読み込み、JMeterとSeleniumを使用してテストを実行します。
テスト結果は、Taurusが提供するダッシュボードやグラフで表示されます。

以下は、Taurusを使用したコマンドラインの例です。

```
bzt test_scenario.yml
```

## まとめ

Taurusを使用することで、JMeterとSeleniumを組み合わせた複雑なテストシナリオを効率的に管理し、自動化することができます。また、Taurusは柔軟な設定と拡張性を持ち、JMeterとSelenium以外のテストツールとも統合することができます。