SeleniumによるAPI呼び出しを含むE2Eテスト自動化

アソビュー! Advent Calendar 2022の2日目(裏面)の記事です。
アソビューでQAをしている渡辺です。
前職ではエンジニア、およびQAをしておりましたが、10月よりアソビューにQAとして入社しました。
今回は、API呼び出しを含むE2Eテストの自動化を、他社ウェブサイトに仕様記載の無料公開APIで試してみた話となります。

アソビューのQAでは、開発スピードと品質向上の両立を図ることを重視しています。
そのためにも、シフトレフトやテスト自動化推進の取り組みは重要です。
現在QAとして参画中のプロジェクトでAPIの外部公開があり、E2EテストとしてAPIを含むテストの自動化はこれまでしていないので、APIを含むテストについて、Seleniumで簡単に自動化できないか試してみました。

なぜSeleniumか?

アソビューでは以前よりAutifyでE2Eテスト自動化をしていますが、API呼び出しには向いていないため、API呼び出しを含むコーディングが可能で無料で始められ、ネット上の情報が多いSelenium+WebDriver+Pythonでまずは気軽に試してみることにしました。

なぜPythonか?

今後のメンテナンスを考えた時に、

  • Seleniumについては、Pythonが一番ネット上の情報が多そう
  • 非エンジニアでもコーディングの差が出にくく、メンテナンスしやすいのではないか

と考えたためです。

試してみるテストケース

インターネット上で一般公開されている無料の天気予報APIで、以下のAPI呼び出しを含むE2Eテストのテストケースを仮設定し、自動化してみます。

天気予報APIの仕様

weather.tsukumijima.net

テスト対象

全都道府県の天気予報

テストケース例

天気予報APIについて

  1. API呼び出しに成功すること
  2. 天気予報APIで取得の直近の最高気温 >= 現在の気温(※1)であること
  3. 天気予報APIで取得の直近の最低気温 <= 現在の気温(※1)であること

(※1) 以下例のように、Googleで各都市の天気を検索時に最上部に表示される気温で確認

環境構築

実行環境は以下になります。

MacBook Pro、M1、macOS Monterey(バージョン12.3)

ネット上の記事も多いため、詳細は割愛しますが、以下を行います。

Pythonインストール&環境設定

PythonをPCにインストールします。

Seleniumインストール

ターミナルで以下コマンドでSeleniumをインストールします。

pip install selenium

WebDriverインストール

ターミナルで以下コマンドでWebDriverをインストールします。
WebDriverは、ブラウザ毎に存在します。
今回はChromeのDriverをインストールします。

pip install chromedriver-binary

API呼び出しを含むテスト自動化をPythonでコーディング

PCローカルにPython実行ファイル(※1)を作成し、テスト自動化をPythonでコーディングしていきます。
大きくは、以下の二手順となります。
(※1)今回例でのファイル名「apitest.py」

手順 内容
API呼び出し 天気予報APIの仕様に合わせて、天気予報のタイトル、最高気温と最低気温をAPI呼び出しにて取得します。
天気予報APIで取得したデータ内容の検証 APIで取得したタイトル(※1)をGoogleでワード検索し最上部に表示された気温が、API取得の最高/最低気温と比較し上記テストケース2,3の範囲内であるかを検証するテストを自動化します。

(※1:例 ・「福岡県 久留米 の天気」)

API呼び出し

最高気温と最低気温を天気予報APIの呼び出しで取得します。

Requestsライブラリをインストール

ターミナルで以下コマンドを実行します

pip install requests

Requestsライブラリをインポート

import requests

HTTP GET メソッドにより天気予報APIを呼び出し

上記、天気予報APIの仕様の例にならい、
ここでは福岡県久留米市(久留米の ID (400040))の天気予報をHTTP GETリクエストによりAPI呼び出しにて取得します。

url = "https://weather.tsukumijima.net/api/forecast/city/400040"
response = requests.get(url)

テストケース1:API呼び出しに成功することの検証

HTTPレスポンスは成功するとステータスコード200が返るので、200でなければエラー表示とします。

assert response.status_code == 200, 'API呼び出し失敗:HTTPステータスコード={0}'.format(response.status_code)

天気タイトルと現在日から直近の予報の最高/最低気温を取得

API呼び出し結果のレスポンスとして天気予報のJSONデータを取得します。

tenki_data = response.json()

JSONデータから、天気タイトルを取得します。

# タイトル・見出し(例・福岡県 久留米 の天気)
title = tenki_data['title']

現在日から直近の天気予報の最高気温と最低気温を取得します。

forecasts = tenki_data['forecasts']
max_celsius = None
min_celsius = None
# 直近の最高気温取得
for forecast in forecasts:
    temp = forecast['temperature']['max']['celsius']
    if (temp):
            max_celsius = temp
        break
# 直近の最低気温取得
for forecast in forecasts:
    temp = forecast['temperature']['min']['celsius']
    if (temp):
            min_celsius = temp
        break

関数化

関数化し、都市毎の処理ができるような構造にします。これまでをまとめると以下のソースとなります。

import requests

# 直近の最高/最低気温取得
def get_temperture(forecasts, target, tag):
    for forecast in forecasts:
        temp = forecast['temperature'][tag]['celsius']
        if (temp):
            target = temp
            break
    return target

# 天気予報API呼び出し 、都市コード毎の最高/最低気温を検証
def tenki_check(city_code):
    #API呼び出し
    url = "https://weather.tsukumijima.net/api/forecast/city/" + city_code
    response = requests.get(url)
    #テストケース1:API呼び出しに成功することの検証
    assert response.status_code == 200, 'API呼び出し失敗:HTTPステータスコード={0}'.format(response.status_code)
    tenki_data = response.json()

    title = tenki_data['title']

    forecasts = tenki_data['forecasts']
    # 天気予報APIより最高気温取得
    max_celsius = None
    max_celsius = get_temperture(forecasts, max_celsius, 'max')
    # 天気予報APIより最低気温取得
    min_celsius = None
    min_celsius = get_temperture(forecasts, min_celsius, 'min')

    print(title)
    print('天気予報API最高気温:',max_celsius)
    print('天気予報API最低気温:',min_celsius)

# 天気予報APIから「福岡県 久留米」の天気情報を取得
city_codes = {"400040"}  #ここにカンマ区切りで各都道府県の都市コードを設定で、各都道府県の天気予報の繰り返し取得が可能
for city_code in city_codes:
    tenki_check(city_code)

天気予報APIで取得したデータ内容の検証

APIで取得したタイトル・見出し(例・福岡県 久留米 の天気)をGoogle検索し、 最上部に表示された気温が範囲内であるかを検証するテストケースを自動化します。


Googleトップページを表示

SeleniumとWebDriverのインポート

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

WebDriverにChromeを取得/設定します

driver = webdriver.Chrome(executable_path="chromedriver")

※以降は、API呼び出し関数化した処理に追記していきます。

Googleトップページ表示

#Google トップページ表示
driver.get("https://www.google.com/")
driver.set_page_load_timeout(10)

Googleワード検索

APIで取得したタイトル・見出し(例・福岡県 久留米 の天気)を検索キーにして、Googleワード検索します。

Googleトップページの検索入力欄のXPathを取得します

  1. Google トップページを表示し、command + option + i でデベロッパーツールを起動
  2. 検索欄を右クリックして検証をクリック
  3. 表示されたコード箇所を右クリックして、[Copy]→[Copy XPath] をクリックでXPathをクリップボードにコピー

Googleトップページの検索入力欄のXPathを指定し、検索入力欄の要素オブジェクトを取得します

# driver.find_element(By.XPATH, '{クリップボードにコピーされたXPathを貼り付け}')
element = driver.find_element(By.XPATH, '/html/body/div[1]/div[3]/form/div[1]/div[1]/div[1]/div/div[2]/input')

GoogleトップページでAPIで取得したタイトル・見出し(例・福岡県 久留米 の天気)を検索入力欄に入力しEnter押下します

element.send_keys(title)
element.send_keys(Keys.ENTER)
driver.set_page_load_timeout(10)

テストケースの実行結果が正しいか検証

検索結果の最上部の現在気温(ウェザーニュース)の要素オブジェクトを取得

#google 検索結果ページから先頭のウェザーニュース天気情報(現在の気温)を取得
element = driver.find_element(By.XPATH, '//*[@id="wob_tm"]')

以下テストケースが正しいか検証

  • 天気予報APIで取得した最高気温より現在気温(ウェザーニュース)が低い
  • 天気予報APIで取得した最低気温より現在気温(ウェザーニュース)が高い
now_temp = element.text
#テストケース2:天気予報APIで取得の直近の最高気温 >= 現在の気温 であることの検証
assert max_celsius >= int(now_temp), '{0} :API取得の最高気温より現在の気温が高いです。'.format(title)
#テストケース3:天気予報APIで取得の直近の最低気温 >= 現在の気温 であることの検証
assert min_celsius <= int(now_temp), '{0} :API取得の最低気温より現在の気温が低いです。'.format(title)

テスト自動化コードの全文

ここまでのテスト自動化コードの全文は以下となります。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

import requests

driver = webdriver.Chrome(executable_path="chromedriver")

# 直近の最高/最低気温取得
def get_temperture(forecasts, target, tag):
    for forecast in forecasts:
        temp = forecast['temperature'][tag]['celsius']
        if (temp):
            target = temp
            break
    return target

# 天気予報API呼び出し 、都市コード毎の最高/最低気温を検証
def tenki_check(city_code):
    #API呼び出し
    url = "https://weather.tsukumijima.net/api/forecast/city/" + city_code
    response = requests.get(url)
    #テストケース1:API呼び出しに成功することの検証
    assert response.status_code == 200, 'API呼び出し失敗:HTTPステータスコード={0}'.format(response.status_code)
    
    tenki_data = response.json()

    # 天気予報APIよりタイトル取得
    title = tenki_data['title']

    forecasts = tenki_data['forecasts']
    # 天気予報APIより最高気温取得
    max_celsius = None
    max_celsius = get_temperture(forecasts, max_celsius, 'max')
    # 天気予報APIより最低気温取得
    min_celsius = None
    min_celsius = get_temperture(forecasts, min_celsius, 'min')

    print(title)
    print('天気予報API最高気温:',max_celsius)
    print('天気予報API最低気温:',min_celsius)

    #Google トップページ 表示
    driver.get("https://www.google.com/")
    driver.set_page_load_timeout(10)

    #Google トップページからワード検索
    element = driver.find_element(By.XPATH, '/html/body/div[1]/div[3]/form/div[1]/div[1]/div[1]/div/div[2]/input')
    element.send_keys(title)
    element.send_keys(Keys.ENTER)
    driver.set_page_load_timeout(10)

    #Google 検索結果ページから先頭のウェザーニュース天気情報(現在の気温)を取得
    element = driver.find_element(By.XPATH, '//*[@id="wob_tm"]')
    now_temp = element.text
    print('現在気温(Googleトップページ(ウェザーニュース)):',now_temp)
    #テストケース2:天気予報APIで取得の直近の最高気温 >= 現在の気温 であることの検証
    assert int(max_celsius) >= int(now_temp), '{0} :API取得の最高気温より現在の気温が高いです。'.format(title)
    #テストケース3:天気予報APIで取得の直近の最低気温 >= 現在の気温 であることの検証
    assert int(min_celsius) <= int(now_temp), '{0} :API取得の最低気温より現在の気温が低いです。'.format(title)

# 天気予報APIから各都道府県の天気情報を取得し、テストケースを検証
# 下記例は、{東京、大阪、名古屋、福岡、京都、仙台、札幌}
city_codes = {"130010","270000","230010","400010","260010","040010","016010"}
for city_code in city_codes:
    tenki_check(city_code)

driver.quit()

自動テスト実行

ターミナルでpython実行ファイルのパスへ移動し、以下コマンドを実行すると、自動テスト実行できます

python apitest.py 

テストケースの検証結果がOKだと、ターミナルにassert文で記載したようなエラー表示がなく、以下のようなログが表示されます

やってみてどうだったか

今回の天気予報APIの例では、都市コードをスプレッドシートで記載しPythonで読み込みをコーディングすれば、データバリエーションを外部ファイルで管理するデータ駆動テストが実現でき、非エンジニアでもメンテナンス可能になりそうです。
また、コーディング可能で自由度が高い分、何秒以内にレスポンスが返るかなど、非機能要求の性能テストも追記しやすいと感じました。

まとめ

現在のプロジェクトで活用できるか

現在のプロジェクトの特性で、API呼び出しを含むテスト自動化を考えると、

  • HTTP POSTメソッドでAPIにてデータ登録
  • E2EテストでAPI経由で登録したデータが画面から参照できる

となりますが、今回試した内容の応用ですぐに活用できると感じました。
都度メンテナンスは必要ですが、特に期待できる点は以下です。

  • 一度作成してしまえばデータバリエーションが増やせ、大量の組み合わせテストがスプリント内で実現できる
  • プロジェクト進行中に何度もテスト済機能のリグレッションテストができる

課題

メンテナンスコストを下げ、非エンジニアでも極力メンテナンスしやすくするためには、以下が必要と感じています。

  • データバリエーションや組み合わせは極力外部ファイル化して、データ駆動テストにする
  • Webページの要素オブジェクト(DOM)の操作の保守性を高める(※1)
  • オブジェクト指向で画面毎にクラス化し、実行や検証はそのクラス化したオブジェクトを跨いで画面遷移前後のテストを可能にする
  • 環境構築を効率的に管理
  • テスト結果が視覚的に分かりやすい工夫

(※1:機械的にXPath取得、またはid属性やclass属性など変更可能性が低ければ、そちらを要素の特定に使用するなど)

最後に

アソビューでは、「生きるに、遊びを。」をテーマに、一緒に働く仲間を募集しています。 QAの募集もありますので、興味ありましたら一度面談でお話ししてみませんか?
アソビュー!サイトにはお得なお出かけ情報がたくさんありますので、週末のお出かけにも是非アソビューをご利用ください!
www.asoview.com