Python+Selenium自動テストで確認画面到達テスト

前回の自動テストからとてつもなく時間がかかったものの、とりあえず入力画面→確認画面までのテストは可能になったのでご報告。

できることになったことは、

  • 入力画面から確認画面に遷移できたかどうか
  • 入力画面で入力した値が確認画面に表示されているかどうか

の2点、十分でしょ、、、ただのメールフォームなら問題ないはず。

スポンサーリンク

更新履歴

日時更新内容
2021-08-21新規作成

大まかなイメージ

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

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

構造

pythonのソース

テストを実行した結果を書き出すlogディレクトリと、画像をアップロードするケースがあった場合、確認用にダウンロードするディレクトリが使われるようになります。

フォーム確認画面のソース一部

確認画面で入力したデータを表示するとき、spanタグで以下のように表示データをくくるようにします。

  <div class="Form-Item">
    <p class="Form-Item-Label"><span class="Form-Item-Label-Required">必須</span>氏名</p>
    <span name="name">山田太郎</span>
  </div>

ソースコード

※コピペで使えるぞ!

import configparser
import datetime
import math
import os
import csv
import re
import cv2

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

# オリジナル例外処理
class FormCheckException(Exception):
    pass


class FormTest:
    cnt = 0
    config = {}
    error_log = None
    result_log = None

    def __init__(self, config):
        date = datetime.datetime.now().strftime("%Y%m%d")

        if not os.path.isdir(os.getcwd() + "\\log"):
            os.mkdir(os.getcwd() + "\\log")

        self.error_log = open(os.getcwd() + "\\log\\" + date + "_error.log.", 'a', encoding="utf-8", newline="")
        self.writeErrorLog("start:" + os.path.basename(__file__))

        self.result_log = open(os.getcwd() + "\\log\\" + date + "_result.log.", 'a', encoding="utf-8", newline="")
        self.writeResultLog("start:" + os.path.basename(__file__))

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

        self.config["settings"] = config["settings"]

    def writeLog(self, message, conn):
        date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        conn.write(date + ":" + message + "\n")
        conn.flush()

    def writeErrorLog(self, message):
        self.writeLog(message, self.error_log)

    def writeResultLog(self, message):
        self.writeLog(message, self.result_log)

    def exec(self, line):
        self.cnt += 1

        # ファイル2行目はデータではないためスキップ
        if (self.cnt == 1):
            self.config["line"] = line
            return False

        # ChromeWebdriverファイルのパス指定
        options = webdriver.ChromeOptions()
        options.add_experimental_option("prefs", {"download.default_directory": os.getcwd() + "\\tmp"})

        driver = webdriver.Chrome(executable_path=os.getcwd() + "\\webdriver\\chromedriver.exe", options=options)
        driver.get(self.config["settings"]["input_url"])

        self.testInput(driver, line)
        self.testConfirm(driver, line)

    def testInput(self, driver, line):

        for key in line:
            if not line[key]:
                continue

            if (self.config["line"][key] in ["text", "email", "password", "textarea", "search", "number"]):
                driver.find_element_by_name(key).send_keys(line[key])

            elif (self.config["line"][key] in ["date", "month", "week", "time", "datetime-local"]):
                # date   →   YYYY-MM-DD
                # month  →   YYYY-MM
                # week  →   YYYY-W01~53
                # time  →   i:s
                # datetime-local    →   YYYY-MM-DDTH:i
                driver.execute_script('document.getElementsByName("' + key + '")[0].value="' + line[key] + '";')

            elif (self.config["line"][key] in ["range"]):
                # rangeはmin、max、step属性と整合性の取れない値はエラー扱いとする
                elem = driver.find_element_by_name(key)

                if elem.get_attribute("min") != "" and int(elem.get_attribute("min")) > int(line[key]):
                    self.writeErrorLog(str(self.cnt) + "行目:入力項目「" + key + "」のテストデータ(" + line[
                        key] + ")は、min属性の数値(" + elem.get_attribute("min") + ")を下回っています")
                elif elem.get_attribute("max") != "" and int(elem.get_attribute("max")) < int(line[key]):
                    self.writeErrorLog(str(self.cnt) + "行目:入力項目「" + key + "」のテストデータ(" + line[
                        key] + ")は、max属性の数値(" + elem.get_attribute("max") + ")を上回っています")
                elif elem.get_attribute("step") != "" and (int(elem.get_attribute("step")) % int(line[key]) != 0):
                    self.writeErrorLog(str(self.cnt) + "行目:入力項目「" + key + "」のテストデータ(" + line[
                        key] + ")は、はstep属性の数値(" + elem.get_attribute("step") + ")で割り切れません")
                else:
                    driver.execute_script('document.getElementsByName("' + key + '")[0].value="' + line[key] + '";')
            elif (self.config["line"][key] in ["color"]):
                # 1文字目#(シャープ)、2~7文字目は16進数で表現できる範囲でないとエラー
                if (len(line[key]) != 7):
                    self.writeErrorLog(str(self.cnt) + "行目:入力項目「" + key + "」のテストデータ(" + line[key] + ")は、7文字である必要があります")
                elif (line[key][:1] != "#"):
                    self.writeErrorLog(str(self.cnt) + "行目:入力項目「" + key + "」のテストデータ(" + line[key] + ")は、1文字目が#ではありません")
                elif not (re.match(".*[0-9A-F].*", line[key][1:].upper())):
                    self.writeErrorLog(
                        str(self.cnt) + "行目:入力項目「" + key + "」のテストデータ(" + line[key] + ")は、2文字目以降が16進数で表現できる範囲にありません")

                driver.find_element_by_name(key).send_keys(line[key])

            elif (self.config["line"][key] in ["file"]):
                if not os.path.isfile(line["file"]):
                    self.writeErrorLog("×"+str(self.cnt) + "行目:入力項目「" + key + "」のテストデータ(" + line["file"] + ")が存在しませんでした")
                else:
                    driver.find_element_by_name(key).send_keys(line[key])

            elif (self.config["line"][key] in ["radio"]):
                elements = driver.find_elements(By.XPATH, "//input[@type='radio'][@name='" + key + "']")

                click_flg = False
                for elem in elements:
                    if (elem.get_attribute("value") == line[key]):
                        click_flg = True
                        elem.click()

                if (click_flg == False):
                    self.writeErrorLog(str(self.cnt) + "行目:入力項目「" + elem.get_attribute(
                        "name") + "」のテストデータ(" + line[key] + ")が見つかりませんでした")

            elif (self.config["line"][key] in ["checkbox"]):
                elements = driver.find_elements(By.XPATH, "//input[@type='checkbox'][@name='" + key + "']")

                for test_param in line[key].split(","):
                    click_flg = False
                    for elem in elements:
                        if (elem.get_attribute("value") == test_param):
                            click_flg = True
                            elem.click()

                    if (click_flg == False):
                        self.writeErrorLog(str(self.cnt) + "行目:入力項目「" + elem.get_attribute(
                            "name") + "」のテストデータ(" + test_param + ")が見つかりませんでした")

            elif (self.config["line"][key] in ["select-one"]):
                elements = driver.find_element(By.XPATH, "//select[@name='" + key + "']")

                for options in elements.find_elements(By.XPATH, "//option"):  # selectタグの内側のoptionタグを取得
                    if (options.get_attribute("value") == line[key]):
                        options.click()
                        break

                if (click_flg == False):
                    self.writeErrorLog(str(self.cnt) + "行目:入力項目「" + elem.get_attribute(
                        "name") + "」のテストデータ(" + test_param + ")が見つかりませんでした")

        driver.find_element_by_id(self.config["settings"]["confirm_move_id"]).click()

        if driver.current_url == self.config["settings"]["confirm_url"]:
            self.writeResultLog(str(self.cnt) + "行目:テストは確認画面に到達しました")
        else:
            self.writeResultLog(str(self.cnt) + "行目:テストは確認画面に到達しませんでした")

    def testConfirm(self, driver, line):
        self.searchFormTags(driver, line)
        driver.quit()

    def diffImage(self, file1, file2):
        if not os.path.exists(file1):
            raise FormCheckException("×" + str(self.cnt) + "行目:" + file1+"が存在しません")
        if not os.path.exists(file2):
            raise FormCheckException("×" + str(self.cnt) + "行目:" + file2+"が存在しません")

        IMG_SIZE = (200, 200)

        file1_img = cv2.imread(file1)
        file1_img = cv2.resize(file1_img, IMG_SIZE)
        file1_hist = cv2.calcHist([file1_img], [0], None, [256], [0, 256])

        file2_img = cv2.imread(file2)
        file2_img = cv2.resize(file2_img, IMG_SIZE)
        file2_hist = cv2.calcHist([file2_img], [0], None, [256], [0, 256])

        ret = cv2.compareHist(file1_hist, file2_hist, 0)

        return math.floor(ret * 100)  # 計算しやすいように*100

    def checkTypeFile(self, driver, line, line_key):
        src = driver.find_element_by_name(line_key).get_attribute("src")

        driver.execute_script("window.open()")
        driver.switch_to.window(driver.window_handles[1])
        driver.get(src)
        img = driver.find_element(By.XPATH, "//img")

        tmp_file = os.getcwd() + "\\tmp\\" + os.path.basename(line[line_key])
        with open(tmp_file, "wb") as w_file:
            w_file.write(img.screenshot_as_png)

        driver.close()
        driver.switch_to.window(driver.window_handles[0])

        # 画像ファイルの比較
        if self.diffImage(tmp_file, line[line_key]) < int(self.config["settings"]["img_hist"]):
            self.writeErrorLog("×"+str(self.cnt) + "行目:入力項目「" + line_key + "」は、画像が一致しない可能性があります")

    def checkTypeRadio(self, line, line_key, span_key):
        self.checkTypeSingleSelect(line, line_key, span_key)

    def checkTypeSelect(self, line, line_key, span_key):
        self.checkTypeSingleSelect(line, line_key, span_key)

    def checkTypeCheckbox(self, line, line_key, span_key):
        self.checkTypeSingleSelect(line, line_key, span_key)

    def checkTypeSingleSelect(self, line, line_key, span_key):
        params = {}
        with open(os.getcwd() + "\\config\\param\\" + self.config["line"][line_key] + "_" + line_key + ".csv",
                  encoding="utf-8") as f:
            for row in csv.reader(f):
                params[row[1]] = row[0]  # テキストをキーに、idを値にする

        check_flg = False
        for value in line[line_key].split(","):
            if span_key.text in params:
                if value in params[span_key.text]:
                    check_flg = True
                    break
            else:
                raise FormCheckException("×" + str(self.cnt) + "行目:" + line_key + "の" + span_key.text + "(" + line[
                    line_key] + ")が確認画面に存在しません(例外)")

        if not check_flg:
            self.writeErrorLog(
                "×" + str(self.cnt) + "行目:" + line_key + "の" + span_key.text + "(" + line[line_key] + ")は一致していません")

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

        for line_key in line:
            try:
                elem = driver.find_elements_by_name(line_key)

                # 確認対象のname属性が確認画面に存在しなければ例外処理
                if len(elem) == 0:
                    raise FormCheckException(
                        "×" + str(self.cnt) + "行目:" + line_key + "の" + line[line_key] + "が確認画面に存在しません(例外)")

                for span_key in elem:
                    if self.config["line"][line_key] in ["radio"]:
                        self.checkTypeRadio(line, line_key, span_key)

                    elif self.config["line"][line_key] in ["select-one"]:
                        self.checkTypeSelect(line, line_key, span_key)

                    elif self.config["line"][line_key] in ["checkbox"]:
                        self.checkTypeCheckbox(line, line_key, span_key)

                    elif self.config["line"][line_key] in ["file"]:
                        self.checkTypeFile(driver, line, line_key)

                    else:
                        if not span_key.text == line[line_key]:
                            self.writeErrorLog("×" + str(self.cnt) + "行目:" + line_key + "の" + line[line_key] + "は一致していません")

            except FormCheckException as inst:
                print(inst.args[0])
                self.writeErrorLog(inst.args[0])

        return elem_fields


config = configparser.ConfigParser()
config.read(os.getcwd() + "\\config\\settings.ini")

with open(os.getcwd() + "\\config\\data.tsv", "r") as f:
    reader = csv.DictReader(f, delimiter="\t")
    test = FormTest(config)
    for line in reader:
        test.exec(line)

    # 終了
    test.writeErrorLog("end:" + os.path.basename(__file__))
    test.writeResultLog("end:" + os.path.basename(__file__))

結果・ログファイルは2種類

結果ファイル

「log/YYYYMMDD.log」と日付ごとに出力します。

2021-08-21 14:14:09:start:FormTest.py
2021-08-21 14:14:12:2行目:テストは確認画面に到達しました
2021-08-21 14:14:17:3行目:テストは確認画面に到達しました

開始のstartと、テスト行ごとに確認画面に到達できたかを出力します。

到達した場合、「テストは確認画面に到達しました」と出力し、テストデータが入力画面のエラーチェックをパスしたことを意味します。

到達しなかった場合、「テストは確認画面に到達しませんでした」と出力し、テストデータが入力画面のエラーチェックでエラー扱いとなったことを意味します。

イメージとしては開発したフォームのエラーチェックを加味して、テストデータの1~30件はエラーチェックをパスするはずのテストデータ、31件~60件はエラーするはずのテストデータを用意し、思惑通りの結果になるかをチェックするような感じです。

ログファイル(エラーログ)

結果ファイルと同じように 「log/YYYYMMDD.log」と日付ごとに出力します。

2021-08-21 14:14:09:start:FormTest.py
2021-08-21 14:14:11:×2行目:入力項目「file」のテストデータ(C:\Users\xxxxx\Desktop\画像.JPG)が存在しませんでした
2021-08-21 14:14:11:×2行目:入力項目「sex」のテストデータ(2)が見つかりませんでした

開始のstartと、もしテストに異常が発生した場合、どのデータがもとで発生したかを出力します。

例えば上記のエラーは、「http://holiday-programmer.net/form_check/」でアップロードするためのテストファイルが存在しないこと、入力画面で選択した性別が確認画面で確認表示されていないことを意味します。

画像ファイルの判定

    def diffImage(self, file1, file2):
        IMG_SIZE = (200, 200)

        file1_img = cv2.imread(file1)
        file1_img = cv2.resize(file1_img, IMG_SIZE)
        file1_hist = cv2.calcHist([file1_img], [0], None, [256], [0, 256])

        file2_img = cv2.imread(file2)
        file2_img = cv2.resize(file2_img, IMG_SIZE)
        file2_hist = cv2.calcHist([file2_img], [0], None, [256], [0, 256])

        ret = cv2.compareHist(file1_hist, file2_hist, 0)

        return math.floor(ret * 100)  # 計算しやすいように*100

画像が入力画面で入力した画像と、確認画面で確認用に表示されている画像が一致するかは、ヒストグラム判定を行います。判定の結果、評価が90以上であれば同じ画像と判定するようにしています。※config/setting.iniのimg_hist値で変更可能

最後100倍にしているのは感覚的に判定しやすいから。。。

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