Python+Selenium自動テストの準備をほぼ自動化する

開発したフォームのテストをしたいとして、Seleniumなどで自動化するにしても準備は結構大変!

例えば、

  • フォームの入力項目はいくつあるのか
  • フォームの入力項目のタイプはどんなものがあるのか
  • 自動テストを実施する際はどのような手段で入力項目を参照すればいいのか

など、フォームごとにたいてい異なるので、いちいち準備していくのは大変と感じます。

そこでテストだけでなく、その準備も自動化したいと思い、ひとまずPythonで組んでみました。

スポンサーリンク

更新履歴

日時更新内容
2021-06-06 新規作成
2021-07-041.設定ファイルの名前を変更
 url.csv → settings.ini
2.テストデータ用ファイルの形式を変更
 data.csv → data.tsv ※やはりtsvこそ至高。。。
2021-07-18テスト用URLを変更
2021-08-21画像判定のヒストグラム閾値をsetting.iniに書き出すよう変更

大まかなイメージ

テストに至るまでの全体イメージは下記の通り

  1. フォームを解析して入力項目の数や種類を明らかにする(※本記事)
  2. 解析しきれなかった情報は手入力する
  3. テストしたいデータを手入力する
  4. テストする

構造

初期に必要な構造

必要な構造は2つ、

  • フォームを解析するFormScan.py
  • webdriverディレクトリにchromewebdriver.exe

を用意する(main.pyはキャプチャミスった)、必要なファイルはFormScan.pyを実行すると作成される

ChromeWebDriverは公式から自分のChromeのバージョンに合致するものを用意する(https://chromedriver.chromium.org/downloads)

※main.pyは無視してよい

ソースコード

※コピペで使えるぞ!

import os
import csv

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


class FormScan:
    def init(self):
        # ディレクトリが存在しない場合、ディレクトリを作成する
        if not os.path.exists(os.getcwd() + "\\config"):
            os.makedirs(os.getcwd() + "\\config")
        if not os.path.exists(os.getcwd() + "\\config\\param"):
            os.makedirs(os.getcwd() + "\\config\\param")

    def writePages(self,driver):
        writer = open(os.getcwd() + "\\config\\url.csv", "w", encoding="utf-8")
        writer.write("[settings]\n")
        writer.write("input_url=" + driver.current_url + "\n")

        #submitが見つかれば記録する
        elem_submit = driver.find_elements(By.XPATH, "//input[@type='submit']")
        if len(elem_submit) == 1:
            #確認画面に遷移するための要素
            if(elem_submit[0].get_attribute("name")!=""):
                writer.write("confirm_move_name="+elem_submit[0].get_attribute("name")+"\n")
            elif(elem_submit[0].get_attribute("id") != ""):
                writer.write("confirm_move_id="+elem_submit[0].get_attribute("id")+"\n")

        else:
            #見つからなければor複数見つかれば空で初期化する
            writer.write("confirm_move_name=\n")

        #formタグが見つかれば確認画面のURLを記録する
        elem_form = driver.find_elements_by_tag_name("form")
        if len(elem_form) == 1:
            writer.write("confirm_url=" + elem_form[0].get_attribute("action") + "\n")
        else:
            writer.write("confirm_url=\n")

        # 画像比較時のヒストグラム閾値
        writer.write("img_hist=90\n")

    def getTypeText(self, driver):
        elem_fields = {}
        target_types = ["text", "tel", "url", "email", "search", "password", "date", "month", "time", "datetime",
                        "datetime-local", "number", "range", "color", "file"]
        for type in target_types:
            elements = driver.find_elements(By.XPATH, "//input[@type='" + type + "']")
            for elem in elements:
                elem_fields[elem.get_attribute("name")] = elem.get_attribute("type")

        return elem_fields

    def getTypeTextarea(self, driver):
        elem_fields = {}
        elements = driver.find_elements(By.XPATH, "//textarea")
        for elem in elements:
            elem_fields[elem.get_attribute("name")] = elem.get_attribute("type")

        return elem_fields

    def getTypeRadio(self, driver):
        return FormScan.execRadioAndCheckbox(self, driver, "radio")

    def getTypeCheckbox(self, driver):
        return FormScan.execRadioAndCheckbox(self, driver, "checkbox")

    def execTextAndTextarea(self, driver, type):
        elem_fields = {}
        elements = driver.find_elements(By.XPATH, "//input[@type='" + type + "']")
        for elem in elements:
            elem_fields[elem.get_attribute("name")] = elem.get_attribute("type")

        return elem_fields

    def execRadioAndCheckbox(self, driver, type):
        elem_fields = {}
        type_arr = {}
        elements = driver.find_elements(By.XPATH, "//input[@type='" + type + "']")
        for elem in elements:
            if elem.get_attribute("name") not in type_arr:
                type_arr[elem.get_attribute("name")] = {}
                elem_fields[elem.get_attribute("name")] = elem.get_attribute(("type"))

            #checkboxがlabelに囲まれているケース
            if len(elem.find_elements_by_xpath("ancestor::label")) != 0:
                type_arr[elem.get_attribute("name")][elem.get_attribute("value")] = elem.find_element_by_xpath(
                    "ancestor::label").text
            #checkboxとlabelが別々の場所に存在するケース
            else:
                labels = driver.find_elements(By.XPATH, "//label")
                for label in labels:
                    if (elem.get_attribute("id") == label.get_attribute("for")):  # idとlabelkが一致したらディクショナリ化
                        type_arr[elem.get_attribute("name")][elem.get_attribute("value")] = label.text

        # パラメータを記録
        for radio in type_arr:
            writer = open(os.getcwd() + "\\config\\param\\" + type + "_" + radio + ".csv", "w", encoding="utf-8")
            for value in type_arr[radio]:
                writer.write(value + "," + type_arr[radio][value] + "\n")

        return elem_fields

    def getTypeTextSelect(self, driver):
        elem_fields = {}
        elements = driver.find_elements(By.XPATH, "//select")
        for elem in elements:
            elem_fields[elem.get_attribute("name")] = elem.get_attribute("type")

            option_arr = elem.find_elements(By.XPATH, "//option")  # selectタグの内側のoptionタグを取得

            writer = open(os.getcwd() + "\\config\\param\\select_" + elem.get_attribute("name") + ".csv", "w", encoding="utf-8")
            for option in option_arr:
                writer.write(option.get_attribute("value") + "," + option.text + "\n")

        return elem_fields

    def searchFormTags(self, driver):
        elem_fields = {}

        # inputタグ処理
        elem_fields.update(FormScan.getTypeText(self, driver))

        # textareaタグ処理
        elem_fields.update(FormScan.getTypeTextarea(self, driver))

        # radioタグ処理
        elem_fields.update(FormScan.getTypeRadio(self, driver))

        # checkboxタグ処理
        elem_fields.update(FormScan.getTypeCheckbox(self, driver))

        # selectタグ処理
        elem_fields.update(FormScan.getTypeTextSelect(self, driver))

        return elem_fields

    def exec(self,url):
        if not os.path.exists(os.getcwd() + "\\webdriver\\chromedriver.exe"):
            print("Error:"+os.getcwd() + "\\webdriver\\chromedriver.exeにWebDriverを配備してください")
            exit()

        # ChromeWebdriverファイルのパス指定
        driver = webdriver.Chrome(executable_path=os.getcwd() + "\\webdriver\\chromedriver.exe")

        # ブラウザを起動し指定のURLを開く
        driver.get(url)

        elements = FormScan.searchFormTags(self, driver)

        with open(os.getcwd() + "\\config\\data.tsv", "w", encoding="utf-8", newline="") as w:
            writer = csv.writer(w,delimiter="\t")
            writer.writerow(elements)
            writer.writerow(elements.values())

        FormScan.writePages(self,driver)

FormScan().init()
FormScan().exec("http://holiday-programmer.net/form_check/")

最後exec()の引数に解析したいフォームのURLを指定する。

実行後はどうなるか

解析した結果が赤枠内のディレクトリ・ファイルとして作成される

config/data.tsvの中身

1行目は入力項目のname属性、2行目はtype属性(textareaなどのtype属性がないものも含む)となり、テストで入力する値は3行目からとなるイメージ。

config/settings.iniの中身

input_url⇒入力画面のURL

confirm_move_id⇒次の画面に遷移するためのid要素

confirm_url⇒次の画面のURL

img_hist→画像判定時に同一画像と判定するヒストグラム閾値

※ confirm_move_id、confirm_urlは解析できなければ=の右が空になる

config/param/以下のファイル群

radio,checkbox,selectなどの選択系の項目は、選択肢の名前とvalue属性が記録される。

スポンサーリンク
おすすめの記事