[Python] WEBアプリケーションをテスト自動化する

# Outline

webのテスト自動化はseleniumでコードベースで作成するものから、キーワード駆動、ローコードツールで作成するものまでいろいろなものがあります。

ここではあえてレトロ（失礼しました）なやり方 Pythonを使ってみた。
素のpythonでやると結構大変であり、おなじpythonでできているrobot frameworkのありがたみを実感できた

# Setup

## Python

Pythonサイトから、Stable Releasesのバージョンをダウンロードする

https://www.python.org/downloads/windows/


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/74029cae-e438-dc81-6aba-e0ace7a7a990.png)

「Add python.exe to PATH」のチェックを入れて、どこからでも実行できるようにする。
その後、「Install Now」をクリックし、インストールを完了させる。

任意の場所でコマンドプロンプトを開き、以下コマンドを実施し、動作確認をする

```
python --version
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/3c7bbc6c-e38a-cb10-96a2-1a12de425574.png)

## library

Pythonで使用するライブラリをインストールする

```
pip install selenium
```

# テストシナリオ

テスト自動化演習サイトを使います。

https://hotel-example-site.takeyaqa.dev/ja/index.html

以下のように、ログインを行うテストを作成します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/23a28d13-884d-fc98-2ac7-0fdb2eacbf8f.png)


# 単純なテストスクリプト

## スクリプト

``` test_login.py
# selenium library
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager

# parameter
URL = 'https://hotel-example-site.takeyaqa.dev/ja/'
EMAIL = 'ichiro@example.com'
PASSWORD = 'password'
USERNAME = '山田一郎'

# web driver setting
options = webdriver.ChromeOptions()
new_driver = ChromeDriverManager().install()
service = ChromeService(executable_path=new_driver)
driver = webdriver.Chrome(service=service , options=options)
driver.maximize_window()

# test scenario
driver.get(URL)
driver.find_element(By.XPATH,"//li[@id='login-holder']/a").click()
# input id & password
driver.find_element(By.XPATH,"//input[@id='email']").send_keys(EMAIL)
driver.find_element(By.XPATH,"//input[@id='password']").send_keys(PASSWORD)
driver.find_element(By.XPATH,"//button[@id='login-button']").click()
# validate login user name
element_text = driver.find_element(By.XPATH,'//p[@id="username"]').text
assert element_text == USERNAME

driver.quit()

```

## 解説

旧来では、chrome driverをダウンロードしてきて、それを指定してwebdriverを動かす必要があった。
ただ、WebDriverManagerを用いると、そのあたりを全部ライブラリで吸収してくれる！

```
new_driver = ChromeDriverManager().install()
```

install library
```
pip install webdriver-manager
```


WebDriverManagerの詳細

https://github.com/SergeyPirogov/webdriver_manager

## テスト実行

では、実行してみましょう

```
python test_login.py
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/171727f8-222b-9608-d6cb-497cab575594.png)

実行結果はこのようになります。
はて？　ローコードツールなど使っていた方にとって、物足らないと思います。
はい、テスト結果がほとんどありません。

では、あえて失敗したときも試してみましょう

```
# parameter
URL = 'https://hotel-example-site.takeyaqa.dev/ja/'
EMAIL = 'ichiro@example.com'
PASSWORD = 'password'
USERNAME = '山田三郎'
````
ログインのUSERNAMEを'山田一郎'から'山田三郎'に変更しました

すると、以下のようにログイン名のEXPECTATIONとACTUALが異なるエラーになることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/467aed46-679d-b839-b3a6-fb72fd9f1cae.png)


まあ、テストはできるけど、これだとテスト結果分析とか大変だなーって思います。
そこで少しアップグレードさせます


# Pytest

pytestは、Python用のオープンソーステストフレームワークで、シンプルかつ柔軟なテスト作成を可能にします。
豊富なプラグインシステムにより、カバレッジ測定や並列テストなどの追加機能も簡単に利用できます。
ユニットテストからインテグレーションテストまで幅広く対応し、効率的なテスト作成と実行をサポートします

pytestを用いると、もっと充実したテストが可能になります

install
```
pip install pytest
pip install pytest-html
```


## fixture

fixtureは、pytestにおいてテストの前後に必要な準備や後片付けを行うための関数です。
Setup , teardownに該当します
fixtureは、テスト関数に引数として渡され、自動的に実行されます。これにより、テストコードの再利用性が高まり、テストの可読性も向上します。

今回、共通処理を conftest.py に定義します


```conftest.py
import pytest
import os
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService

from webdriver_manager.chrome import ChromeDriverManager
from dotenv import load_dotenv

@pytest.fixture(scope="session")
def driver():
    load_dotenv()
    
    options = webdriver.ChromeOptions()

    new_driver = ChromeDriverManager().install()
    service = ChromeService(executable_path=new_driver)
    driver = webdriver.Chrome(service=service , options=options)
    driver.maximize_window()
    driver.implicitly_wait(3)
    
    print("[SET UP] finish loading driver.")
    
    yield driver

    print("[TEAR DWON] finish loading driver.")
    driver.quit()

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    pytest_html = item.config.pluginmanager.getplugin('html')
    # get test result
    outcome = yield
    report = outcome.get_result()
    extra = getattr(report, 'extra', [])
    
    # save screen shot when test is failed
    if report.when == 'call' and report.failed:
        driver = item.funcargs['driver'] 
        screenshot_path = os.path.join(os.getcwd(),  f"{item.name}.png")
        print("[MAKE REPORT] save screenshot " + screenshot_path)
        driver.save_screenshot(screenshot_path)
        screenshot = driver.get_screenshot_as_base64()
        extra.append(pytest_html.extras.image(screenshot, ''))
        report.extra = extra
        report.extra.append(pytest_html.extras.image(screenshot_path))
```

2つ共通処理を定義しています

### driver

起動時のwebdriverを設定します

### pytest_runtest_makereport

テスト失敗時に自動的にスクリーンショットを保存します

## テスト本体

他のテストでも用いる共有部分(setup / teardown)をconftest.pyに外だしすることができたため、テスト本体はシンプルになります

```test_login_pytest.py
import os
from selenium.webdriver.common.by import By

URL = 'https://hotel.testplanisphere.dev/ja/index.html'
EMAIL = 'ichiro@example.com'
PASSWORD = 'password'
USERNAME = '山田一郎'

def test_login(driver):
    driver.get(URL)
    driver.find_element(By.XPATH,"//*[@id='login-holder']/a").click()
    # input id & password
    driver.find_element(By.XPATH,"//*[@id='email']").send_keys(EMAIL)
    driver.find_element(By.XPATH,"//*[@id='password']").send_keys(PASSWORD)
    driver.find_element(By.XPATH,"//*[@id='login-button']").click()
    # validate
    element_text = driver.find_element(By.XPATH,"//p[@id='username']").text
    assert element_text == USERNAME

    screenshot_path = os.path.join(os.getcwd(),  f"success.png")
    driver.save_screenshot(screenshot_path)

    return

if __name__ == "__main__":
    test_login()

```

## テスト実行

```
pytest test_login_pytest.py --html=report.html
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/24937f8d-48e7-e346-2c95-875b7e068095.png)


pytestを行うことで、レポートhtmlも作成されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/9689ceed-181a-7152-a4e9-7ed863de1d8d.png)


では、失敗した例です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/c9de386b-8a6a-0460-26ed-77dd3b79302b.png)

レポートhtmlはこちら

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/677cf13c-a995-3d3d-e361-8cc57db4f745.png)

また、失敗したときのスクショはこちら

![test_login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/d1460194-586e-f6e2-a4a6-cee2bf001f44.png)


このように、pytestを用いると、スクリプトの保守性を上げ、かつ実行結果作成まで対応してくれる。

ただ、ある程度基盤を作りこまないといけないため、robot frameworkなどのありがたみを感じた。